From 251a89eaf4702cd338b30a6635dbf59acbb48817 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 3 Nov 2020 16:17:35 +1100 Subject: [PATCH 001/147] Updated comment on Settings class --- src/helpers/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/Settings.js b/src/helpers/Settings.js index ffd029b..348fc3f 100644 --- a/src/helpers/Settings.js +++ b/src/helpers/Settings.js @@ -1,7 +1,7 @@ import FakeStorage from "./FakeStorage"; /** - * An interface to a local storage back settings store with a versioning mechanism + * An interface to a local storage backed settings store with a versioning mechanism */ class Settings { name; From 9c1960ba71997032fa675c95a0112151b5d9f956 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 3 Nov 2020 16:20:51 +1100 Subject: [PATCH 002/147] Refactored getBrushPosition helper --- src/components/map/MapDrawing.js | 4 ++-- src/components/map/MapFog.js | 4 ++-- src/components/map/MapMeasure.js | 4 ++-- src/helpers/drawing.js | 15 ++------------- 4 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index 11c54df..67362aa 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -55,8 +55,8 @@ function MapDrawing({ return getBrushPositionForTool( map, getRelativePointerPositionNormalized(mapImage), - toolId, - toolSettings, + map.snapToGrid && isShape, + false, gridSize, shapes ); diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index be9e043..e183071 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -61,8 +61,8 @@ function MapFog({ return getBrushPositionForTool( map, getRelativePointerPositionNormalized(mapImage), - toolId, - toolSettings, + map.snapToGrid && toolSettings.type === "polygon", + toolSettings.useEdgeSnapping, gridSize, shapes ); diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js index c3544a7..bd96019 100644 --- a/src/components/map/MapMeasure.js +++ b/src/components/map/MapMeasure.js @@ -38,8 +38,8 @@ function MapMeasure({ map, selectedToolSettings, active, gridSize }) { return getBrushPositionForTool( map, getRelativePointerPositionNormalized(mapImage), - "drawing", - { type: "line" }, + map.snapToGrid, + false, gridSize, [] ); diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index 50c7e57..8ab54bf 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -8,22 +8,13 @@ const snappingThreshold = 1 / 5; export function getBrushPositionForTool( map, brushPosition, - tool, - toolSettings, + useGridSnappning, + useEdgeSnapping, gridSize, shapes ) { let position = brushPosition; - const useGridSnappning = - map.snapToGrid && - ((tool === "drawing" && - (toolSettings.type === "line" || - toolSettings.type === "rectangle" || - toolSettings.type === "circle" || - toolSettings.type === "triangle")) || - (tool === "fog" && toolSettings.type === "polygon")); - if (useGridSnappning) { // Snap to corners of grid // Subtract offset to transform into offset space then add it back transform back @@ -58,8 +49,6 @@ export function getBrushPositionForTool( } } - const useEdgeSnapping = tool === "fog" && toolSettings.useEdgeSnapping; - if (useEdgeSnapping) { const minGrid = Vector2.min(gridSize); let closestDistance = Number.MAX_VALUE; From 9fbd0d2a9d6f600ea79bd48aa4861509d4ca1f1c Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 3 Nov 2020 16:21:39 +1100 Subject: [PATCH 003/147] Added notes tool setup --- src/components/map/Map.js | 12 +++ src/components/map/MapControls.js | 10 ++- src/components/map/MapInteraction.js | 7 ++ src/components/map/MapNotes.js | 87 +++++++++++++++++++ .../map/controls/NoteToolSettings.js | 47 ++++++++++ src/icons/MoveIcon.js | 19 ++++ src/icons/NoteAddIcon.js | 23 +++++ src/icons/NoteToolIcon.js | 19 ++++ src/settings.js | 5 ++ 9 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 src/components/map/MapNotes.js create mode 100644 src/components/map/controls/NoteToolSettings.js create mode 100644 src/icons/MoveIcon.js create mode 100644 src/icons/NoteAddIcon.js create mode 100644 src/icons/NoteToolIcon.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index ba2fb2d..78fc6bf 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -10,6 +10,7 @@ import MapGrid from "./MapGrid"; import MapMeasure from "./MapMeasure"; import MapLoadingOverlay from "./MapLoadingOverlay"; import NetworkedMapPointer from "../../network/NetworkedMapPointer"; +import MapNotes from "./MapNotes"; import TokenDataContext from "../../contexts/TokenDataContext"; import SettingsContext from "../../contexts/SettingsContext"; @@ -133,6 +134,7 @@ function Map({ disabledControls.push("pan"); disabledControls.push("measure"); disabledControls.push("pointer"); + disabledControls.push("note"); } if (!allowFogDrawing) { disabledControls.push("fog"); @@ -350,6 +352,15 @@ function Map({ /> ); + const mapNotes = ( + + ); + return ( {mapGrid} + {mapNotes} {mapDrawing} {mapTokens} {mapFog} diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js index a912201..80e4840 100644 --- a/src/components/map/MapControls.js +++ b/src/components/map/MapControls.js @@ -9,6 +9,7 @@ import SelectMapButton from "./SelectMapButton"; import FogToolSettings from "./controls/FogToolSettings"; import DrawingToolSettings from "./controls/DrawingToolSettings"; import MeasureToolSettings from "./controls/MeasureToolSettings"; +import NoteToolSettings from "./controls/NoteToolSettings"; import PanToolIcon from "../../icons/PanToolIcon"; import FogToolIcon from "../../icons/FogToolIcon"; @@ -18,6 +19,7 @@ import ExpandMoreIcon from "../../icons/ExpandMoreIcon"; import PointerToolIcon from "../../icons/PointerToolIcon"; import FullScreenIcon from "../../icons/FullScreenIcon"; import FullScreenExitIcon from "../../icons/FullScreenExitIcon"; +import NoteToolIcon from "../../icons/NoteToolIcon"; import useSetting from "../../helpers/useSetting"; @@ -66,8 +68,14 @@ function MapContols({ icon: , title: "Pointer Tool (Q)", }, + note: { + id: "note", + icon: , + title: "Note Tool (N)", + SettingsComponent: NoteToolSettings, + }, }; - const tools = ["pan", "fog", "drawing", "measure", "pointer"]; + const tools = ["pan", "fog", "drawing", "measure", "pointer", "note"]; const sections = [ { diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index b750f6d..6be1a86 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -135,6 +135,9 @@ function MapInteraction({ if (event.key === "q" && !disabledControls.includes("pointer")) { onSelectedToolChange("pointer"); } + if (event.key === "n" && !disabledControls.includes("note")) { + onSelectedToolChange("note"); + } } function handleKeyUp(event) { @@ -153,6 +156,10 @@ function MapInteraction({ return "move"; case "fog": case "drawing": + case "note": + return settings.settings[tool].type === "move" + ? "pointer" + : "crosshair"; case "measure": case "pointer": return "crosshair"; diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js new file mode 100644 index 0000000..8d3ba34 --- /dev/null +++ b/src/components/map/MapNotes.js @@ -0,0 +1,87 @@ +import React, { useContext, useState, useEffect } from "react"; +import { Group, Rect } from "react-konva"; + +import MapInteractionContext from "../../contexts/MapInteractionContext"; +import MapStageContext from "../../contexts/MapStageContext"; + +import { getBrushPositionForTool } from "../../helpers/drawing"; +import { getRelativePointerPositionNormalized } from "../../helpers/konva"; + +const defaultNoteSize = 2; + +function MapNotes({ map, selectedToolSettings, active, gridSize }) { + const { mapWidth, mapHeight, interactionEmitter } = useContext( + MapInteractionContext + ); + const mapStageRef = useContext(MapStageContext); + const [isBrushDown, setIsBrushDown] = useState(false); + const [brushPosition, setBrushPosition] = useState({ x: 0, y: 0 }); + + useEffect(() => { + if (!active) { + return; + } + const mapStage = mapStageRef.current; + + function getBrushPosition() { + const mapImage = mapStage.findOne("#mapImage"); + return getBrushPositionForTool( + map, + getRelativePointerPositionNormalized(mapImage), + map.snapToGrid, + false, + gridSize, + [] + ); + } + + function handleBrushDown() { + setBrushPosition(getBrushPosition()); + setIsBrushDown(true); + } + + function handleBrushMove() { + setBrushPosition(getBrushPosition()); + setIsBrushDown(true); + } + + function handleBrushUp() { + setBrushPosition({ x: 0, y: 0 }); + setIsBrushDown(false); + } + + interactionEmitter.on("dragStart", handleBrushDown); + interactionEmitter.on("drag", handleBrushMove); + interactionEmitter.on("dragEnd", handleBrushUp); + + return () => { + interactionEmitter.off("dragStart", handleBrushDown); + interactionEmitter.off("drag", handleBrushMove); + interactionEmitter.off("dragEnd", handleBrushUp); + }; + }); + + const noteWidth = map && (mapWidth / map.grid.size.x) * defaultNoteSize; + const noteHeight = map && (mapHeight / map.grid.size.y) * defaultNoteSize; + + return ( + + {isBrushDown && ( + + )} + + ); +} + +export default MapNotes; diff --git a/src/components/map/controls/NoteToolSettings.js b/src/components/map/controls/NoteToolSettings.js new file mode 100644 index 0000000..869fd01 --- /dev/null +++ b/src/components/map/controls/NoteToolSettings.js @@ -0,0 +1,47 @@ +import React from "react"; +import { Flex } from "theme-ui"; + +import ToolSection from "./ToolSection"; +import NoteAddIcon from "../../../icons/NoteAddIcon"; +import MoveIcon from "../../../icons/MoveIcon"; + +import useKeyboard from "../../../helpers/useKeyboard"; + +function NoteToolSettings({ settings, onSettingChange }) { + // Keyboard shortcuts + function handleKeyDown({ key }) { + if (key === "a") { + onSettingChange({ type: "add" }); + } else if (key === "v") { + onSettingChange({ type: "move" }); + } + } + + useKeyboard(handleKeyDown); + + const tools = [ + { + id: "add", + title: "Add Note (A)", + isSelected: settings.type === "add", + icon: , + }, + { + id: "move", + title: "Move Note (V)", + isSelected: settings.type === "move", + icon: , + }, + ]; + + return ( + + onSettingChange({ type: tool.id })} + /> + + ); +} + +export default NoteToolSettings; diff --git a/src/icons/MoveIcon.js b/src/icons/MoveIcon.js new file mode 100644 index 0000000..233a691 --- /dev/null +++ b/src/icons/MoveIcon.js @@ -0,0 +1,19 @@ +import React from "react"; + +function MoveIcon() { + return ( + + + + + ); +} + +export default MoveIcon; diff --git a/src/icons/NoteAddIcon.js b/src/icons/NoteAddIcon.js new file mode 100644 index 0000000..6ad5e28 --- /dev/null +++ b/src/icons/NoteAddIcon.js @@ -0,0 +1,23 @@ +import React from "react"; + +function NoteAddIcon() { + return ( + + + + + + + + + ); +} + +export default NoteAddIcon; diff --git a/src/icons/NoteToolIcon.js b/src/icons/NoteToolIcon.js new file mode 100644 index 0000000..e9bb0e5 --- /dev/null +++ b/src/icons/NoteToolIcon.js @@ -0,0 +1,19 @@ +import React from "react"; + +function NoteToolIcon() { + return ( + + + + + ); +} + +export default NoteToolIcon; diff --git a/src/settings.js b/src/settings.js index 16bfa7c..e865967 100644 --- a/src/settings.js +++ b/src/settings.js @@ -32,6 +32,11 @@ function loadVersions(settings) { ...prev, map: { fullScreen: false, labelSize: 1 }, })); + // v1.7.0 - Added note tool + settings.version(3, (prev) => ({ + ...prev, + note: { type: "add" }, + })); } export function getSettings() { From 84af4c58451236b65dec63b8221b07fa848917d6 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 3 Nov 2020 16:38:26 +1100 Subject: [PATCH 004/147] Removed unused props from map drawing and fog --- src/components/map/Map.js | 2 -- src/components/map/MapDrawing.js | 1 - src/components/map/MapFog.js | 1 - 3 files changed, 4 deletions(-) diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 78fc6bf..7a35759 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -311,7 +311,6 @@ function Map({ onShapeAdd={handleMapShapeAdd} onShapesRemove={handleMapShapesRemove} active={selectedToolId === "drawing"} - toolId="drawing" toolSettings={settings.drawing} gridSize={gridSizeNormalized} /> @@ -326,7 +325,6 @@ function Map({ onShapesRemove={handleFogShapesRemove} onShapesEdit={handleFogShapesEdit} active={selectedToolId === "fog"} - toolId="fog" toolSettings={settings.fog} gridSize={gridSizeNormalized} transparent={allowFogDrawing && !settings.fog.preview} diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index 67362aa..8e8f71e 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -23,7 +23,6 @@ function MapDrawing({ onShapeAdd, onShapesRemove, active, - toolId, toolSettings, gridSize, }) { diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index e183071..f209914 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -30,7 +30,6 @@ function MapFog({ onShapesRemove, onShapesEdit, active, - toolId, toolSettings, gridSize, transparent, From 6c1d9528550103aac76f256ce4fddf060eb9c1bb Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 3 Nov 2020 17:15:39 +1100 Subject: [PATCH 005/147] Added basic persistance to notes --- src/components/map/Map.js | 4 ++ src/components/map/MapNote.js | 31 +++++++++++++ src/components/map/MapNotes.js | 66 +++++++++++++++++----------- src/database.js | 11 +++++ src/network/NetworkedMapAndTokens.js | 21 +++++++++ 5 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 src/components/map/MapNote.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 7a35759..05f806d 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -33,6 +33,7 @@ function Map({ onFogDraw, onFogDrawUndo, onFogDrawRedo, + onMapNoteAdd, allowMapDrawing, allowFogDrawing, allowMapChange, @@ -356,6 +357,9 @@ function Map({ active={selectedToolId === "note"} gridSize={gridSizeNormalized} selectedToolSettings={settings[selectedToolId]} + onNoteAdd={onMapNoteAdd} + // TODO: Sort by last modified + notes={mapState ? Object.values(mapState.notes) : []} /> ); diff --git a/src/components/map/MapNote.js b/src/components/map/MapNote.js new file mode 100644 index 0000000..bd95179 --- /dev/null +++ b/src/components/map/MapNote.js @@ -0,0 +1,31 @@ +import React, { useContext } from "react"; +import { Group, Rect } from "react-konva"; + +import MapInteractionContext from "../../contexts/MapInteractionContext"; + +function MapNote({ note, map }) { + const { mapWidth, mapHeight } = useContext(MapInteractionContext); + + const noteWidth = map && (mapWidth / map.grid.size.x) * note.size; + const noteHeight = map && (mapHeight / map.grid.size.y) * note.size; + + return ( + + + + ); +} + +export default MapNote; diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index 8d3ba34..64a0463 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -1,21 +1,31 @@ import React, { useContext, useState, useEffect } from "react"; -import { Group, Rect } from "react-konva"; +import shortid from "shortid"; +import { Group } from "react-konva"; import MapInteractionContext from "../../contexts/MapInteractionContext"; import MapStageContext from "../../contexts/MapStageContext"; +import AuthContext from "../../contexts/AuthContext"; import { getBrushPositionForTool } from "../../helpers/drawing"; import { getRelativePointerPositionNormalized } from "../../helpers/konva"; +import MapNote from "./MapNote"; + const defaultNoteSize = 2; -function MapNotes({ map, selectedToolSettings, active, gridSize }) { - const { mapWidth, mapHeight, interactionEmitter } = useContext( - MapInteractionContext - ); +function MapNotes({ + map, + selectedToolSettings, + active, + gridSize, + onNoteAdd, + notes, +}) { + const { interactionEmitter } = useContext(MapInteractionContext); + const { userId } = useContext(AuthContext); const mapStageRef = useContext(MapStageContext); const [isBrushDown, setIsBrushDown] = useState(false); - const [brushPosition, setBrushPosition] = useState({ x: 0, y: 0 }); + const [noteData, setNoteData] = useState(null); useEffect(() => { if (!active) { @@ -36,17 +46,34 @@ function MapNotes({ map, selectedToolSettings, active, gridSize }) { } function handleBrushDown() { - setBrushPosition(getBrushPosition()); + const brushPosition = getBrushPosition(); + setNoteData({ + x: brushPosition.x, + y: brushPosition.y, + size: defaultNoteSize, + text: "", + id: shortid.generate(), + lastModified: Date.now(), + lastModifiedBy: userId, + visible: true, + locked: false, + }); setIsBrushDown(true); } function handleBrushMove() { - setBrushPosition(getBrushPosition()); + const brushPosition = getBrushPosition(); + setNoteData((prev) => ({ + ...prev, + x: brushPosition.x, + y: brushPosition.y, + })); setIsBrushDown(true); } function handleBrushUp() { - setBrushPosition({ x: 0, y: 0 }); + onNoteAdd(noteData); + setNoteData(null); setIsBrushDown(false); } @@ -61,25 +88,12 @@ function MapNotes({ map, selectedToolSettings, active, gridSize }) { }; }); - const noteWidth = map && (mapWidth / map.grid.size.x) * defaultNoteSize; - const noteHeight = map && (mapHeight / map.grid.size.y) * defaultNoteSize; - return ( - {isBrushDown && ( - - )} + {notes.map((note) => ( + + ))} + {isBrushDown && noteData && } ); } diff --git a/src/database.js b/src/database.js index 8c233fd..91cf035 100644 --- a/src/database.js +++ b/src/database.js @@ -268,6 +268,17 @@ function loadVersions(db) { token.height = tokenSizes[token.id].height; }); }); + // v1.7.0 - Added note tool + db.version(16) + .stores({}) + .upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + state.notes = {}; + }); + }); } // Get the dexie database used in DatabaseContext diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 0640690..98c992a 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -167,6 +167,17 @@ function NetworkedMapAndTokens({ session }) { session.send("mapFogIndex", index); } + function handleNoteAdd(note) { + setCurrentMapState((prevMapState) => ({ + ...prevMapState, + notes: { + ...prevMapState.notes, + [note.id]: note, + }, + })); + session.send("mapNoteAdd", note); + } + /** * Token state */ @@ -395,6 +406,15 @@ function NetworkedMapAndTokens({ session }) { fogDrawActionIndex: data, })); } + if (id === "mapNoteAdd" && currentMapState) { + setCurrentMapState((prevMapState) => ({ + ...prevMapState, + notes: { + ...prevMapState.notes, + [data.id]: data, + }, + })); + } } function handlePeerDataProgress({ id, total, count }) { @@ -460,6 +480,7 @@ function NetworkedMapAndTokens({ session }) { onFogDraw={handleFogDraw} onFogDrawUndo={handleFogDrawUndo} onFogDrawRedo={handleFogDrawRedo} + onMapNoteAdd={handleNoteAdd} allowMapDrawing={canEditMapDrawing} allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap} From 92fa7133bc54b0bec4b9bb30d0200115c4c8d240 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 4 Nov 2020 15:02:56 +1100 Subject: [PATCH 006/147] Added notes to default map state --- src/contexts/MapDataContext.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index d9e2092..a9a3621 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -20,6 +20,7 @@ const defaultMapState = { fogDrawActions: [], // Flags to determine what other people can edit editFlags: ["drawing", "tokens"], + notes: {}, }; export function MapDataProvider({ children }) { From 927c596b04fcacb844c759b6eb25a37570a134dc Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 4 Nov 2020 15:03:14 +1100 Subject: [PATCH 007/147] Added notes to map state reset detection --- src/components/map/MapTiles.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js index aba9e16..e88a75d 100644 --- a/src/components/map/MapTiles.js +++ b/src/components/map/MapTiles.js @@ -39,7 +39,8 @@ function MapTiles({ if ( Object.values(state.tokens).length > 0 || state.mapDrawActions.length > 0 || - state.fogDrawActions.length > 0 + state.fogDrawActions.length > 0 || + Object.values(state.notes).length > 0 ) { hasMapState = true; break; From a3ae3471e8ad113bea95a82ab6c634ec3ddf49af Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 4 Nov 2020 15:03:34 +1100 Subject: [PATCH 008/147] Added note menu and note text --- src/components/map/Map.js | 26 +++- src/components/map/MapNote.js | 130 ++++++++++++++-- src/components/map/MapNoteMenu.js | 216 +++++++++++++++++++++++++++ src/components/map/MapNotes.js | 71 ++++++--- src/network/NetworkedMapAndTokens.js | 8 +- 5 files changed, 411 insertions(+), 40 deletions(-) create mode 100644 src/components/map/MapNoteMenu.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 05f806d..710813f 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -19,6 +19,7 @@ import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; import { drawActionsToShapes } from "../../helpers/drawing"; +import MapNoteMenu from "./MapNoteMenu"; function Map({ map, @@ -33,7 +34,7 @@ function Map({ onFogDraw, onFogDrawUndo, onFogDrawRedo, - onMapNoteAdd, + onMapNoteChange, allowMapDrawing, allowFogDrawing, allowMapChange, @@ -351,15 +352,35 @@ function Map({ /> ); + const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); + const [noteMenuOptions, setNoteMenuOptions] = useState({}); + function handleNoteMenuOpen(noteId, noteNode) { + setNoteMenuOptions({ noteId, noteNode }); + setIsNoteMenuOpen(true); + } + const mapNotes = ( + ); + + const noteMenu = ( + setIsNoteMenuOpen(false)} + onNoteChange={onMapNoteChange} + note={mapState && mapState.notes[noteMenuOptions.noteId]} + noteNode={noteMenuOptions.noteNode} + map={map} /> ); @@ -370,6 +391,7 @@ function Map({ <> {mapControls} {tokenMenu} + {noteMenu} {tokenDragOverlay} diff --git a/src/components/map/MapNote.js b/src/components/map/MapNote.js index bd95179..b2f5fba 100644 --- a/src/components/map/MapNote.js +++ b/src/components/map/MapNote.js @@ -1,29 +1,139 @@ -import React, { useContext } from "react"; -import { Group, Rect } from "react-konva"; +import React, { useContext, useEffect, useState, useRef } from "react"; +import { Group, Rect, Text } from "react-konva"; +import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; -function MapNote({ note, map }) { +import * as Vector2 from "../../helpers/vector2"; +import colors from "../../helpers/colors"; + +const snappingThreshold = 1 / 5; +const textPadding = 4; + +function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { + const { userId } = useContext(AuthContext); const { mapWidth, mapHeight } = useContext(MapInteractionContext); const noteWidth = map && (mapWidth / map.grid.size.x) * note.size; const noteHeight = map && (mapHeight / map.grid.size.y) * note.size; + function handleClick(event) { + if (draggable) { + const noteNode = event.target; + onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode); + } + } + + function handleDragMove(event) { + const noteGroup = event.target; + // Snap to corners of grid + if (map.snapToGrid) { + const offset = Vector2.multiply(map.grid.inset.topLeft, { + x: mapWidth, + y: mapHeight, + }); + const position = { + x: noteGroup.x() + noteGroup.width() / 2, + y: noteGroup.y() + noteGroup.height() / 2, + }; + const gridSize = { + x: + (mapWidth * + (map.grid.inset.bottomRight.x - map.grid.inset.topLeft.x)) / + map.grid.size.x, + y: + (mapHeight * + (map.grid.inset.bottomRight.y - map.grid.inset.topLeft.y)) / + map.grid.size.y, + }; + // Transform into offset space, round, then transform back + const gridSnap = Vector2.add( + Vector2.roundTo(Vector2.subtract(position, offset), gridSize), + offset + ); + const gridDistance = Vector2.length(Vector2.subtract(gridSnap, position)); + const minGrid = Vector2.min(gridSize); + if (gridDistance < minGrid * snappingThreshold) { + noteGroup.x(gridSnap.x - noteGroup.width() / 2); + noteGroup.y(gridSnap.y - noteGroup.height() / 2); + } + } + } + + function handleDragEnd(event) { + const noteGroup = event.target; + onNoteChange && + onNoteChange({ + ...note, + x: noteGroup.x() / mapWidth, + y: noteGroup.y() / mapHeight, + lastModifiedBy: userId, + lastModified: Date.now(), + }); + } + + const [fontSize, setFontSize] = useState(1); + useEffect(() => { + const text = textRef.current; + function findFontSize() { + // Create an array from 4 to 18 scaled to the note size + const sizes = Array.from( + { length: 14 * note.size }, + (_, i) => i + 4 * note.size + ); + + return sizes.reduce((prev, curr) => { + text.fontSize(curr); + const width = text.getTextWidth() + textPadding * 2; + if (width < noteWidth) { + return curr; + } else { + return prev; + } + }); + } + + setFontSize(findFontSize()); + }, [note, noteWidth]); + + const textRef = useRef(); + return ( - + + + {/* Use an invisible text block to work out text sizing */} + ); } diff --git a/src/components/map/MapNoteMenu.js b/src/components/map/MapNoteMenu.js new file mode 100644 index 0000000..d02afec --- /dev/null +++ b/src/components/map/MapNoteMenu.js @@ -0,0 +1,216 @@ +import React, { useEffect, useState, useContext } from "react"; +import { Box, Input, Slider, Flex, Text, IconButton } from "theme-ui"; + +import MapMenu from "../map/MapMenu"; + +import colors, { colorOptions } from "../../helpers/colors"; + +import usePrevious from "../../helpers/usePrevious"; + +import LockIcon from "../../icons/TokenLockIcon"; +import UnlockIcon from "../../icons/TokenUnlockIcon"; +import ShowIcon from "../../icons/TokenShowIcon"; +import HideIcon from "../../icons/TokenHideIcon"; + +import AuthContext from "../../contexts/AuthContext"; + +const defaultNoteMaxSize = 6; + +function MapNoteMenu({ + isOpen, + onRequestClose, + note, + noteNode, + onNoteChange, + map, +}) { + const { userId } = useContext(AuthContext); + + const wasOpen = usePrevious(isOpen); + + const [noteMaxSize, setNoteMaxSize] = useState(defaultNoteMaxSize); + const [menuLeft, setMenuLeft] = useState(0); + const [menuTop, setMenuTop] = useState(0); + useEffect(() => { + if (isOpen && !wasOpen && note) { + setNoteMaxSize(Math.max(note.size, defaultNoteMaxSize)); + // Update menu position + if (noteNode) { + const nodeRect = noteNode.getClientRect(); + const mapElement = document.querySelector(".map"); + const mapRect = mapElement.getBoundingClientRect(); + + // Center X for the menu which is 156px wide + setMenuLeft(mapRect.left + nodeRect.x + nodeRect.width / 2 - 156 / 2); + // Y 12px from the bottom + setMenuTop(mapRect.top + nodeRect.y + nodeRect.height + 12); + } + } + }, [isOpen, note, wasOpen, noteNode]); + + function handleTextChange(event) { + const text = event.target.value; + note && onNoteChange({ ...note, text: text }); + } + + function handleColorChange(color) { + if (!note) { + return; + } + onNoteChange({ ...note, color: color }); + } + + function handleSizeChange(event) { + const newSize = parseInt(event.target.value); + note && onNoteChange({ ...note, size: newSize }); + } + + function handleVisibleChange() { + note && onNoteChange({ ...note, visible: !note.visible }); + } + + function handleLockChange() { + note && onNoteChange({ ...note, locked: !note.locked }); + } + + function handleModalContent(node) { + if (node) { + // Focus input + const tokenLabelInput = node.querySelector("#changeNoteText"); + tokenLabelInput.focus(); + tokenLabelInput.select(); + + // Ensure menu is in bounds + const nodeRect = node.getBoundingClientRect(); + const mapElement = document.querySelector(".map"); + const mapRect = mapElement.getBoundingClientRect(); + setMenuLeft((prevLeft) => + Math.min( + mapRect.right - nodeRect.width, + Math.max(mapRect.left, prevLeft) + ) + ); + setMenuTop((prevTop) => + Math.min(mapRect.bottom - nodeRect.height, prevTop) + ); + } + } + + return ( + + + { + e.preventDefault(); + onRequestClose(); + }} + sx={{ alignItems: "center" }} + > + + Label: + + + + + {colorOptions.map((color) => ( + handleColorChange(color)} + aria-label={`Note label Color ${color}`} + > + {note && note.color === color && ( + + )} + + ))} + + + + Size: + + + + {/* Only show hide and lock token actions to map owners */} + {map && map.owner === userId && ( + + + {note && note.visible ? : } + + + {note && note.locked ? : } + + + )} + + + ); +} + +export default MapNoteMenu; diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index 64a0463..64b1591 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -1,4 +1,4 @@ -import React, { useContext, useState, useEffect } from "react"; +import React, { useContext, useState, useEffect, useRef } from "react"; import shortid from "shortid"; import { Group } from "react-konva"; @@ -19,7 +19,9 @@ function MapNotes({ active, gridSize, onNoteAdd, + onNoteChange, notes, + onNoteMenuOpen, }) { const { interactionEmitter } = useContext(MapInteractionContext); const { userId } = useContext(AuthContext); @@ -27,6 +29,8 @@ function MapNotes({ const [isBrushDown, setIsBrushDown] = useState(false); const [noteData, setNoteData] = useState(null); + const creatingNoteRef = useRef(); + useEffect(() => { if (!active) { return; @@ -46,33 +50,41 @@ function MapNotes({ } function handleBrushDown() { - const brushPosition = getBrushPosition(); - setNoteData({ - x: brushPosition.x, - y: brushPosition.y, - size: defaultNoteSize, - text: "", - id: shortid.generate(), - lastModified: Date.now(), - lastModifiedBy: userId, - visible: true, - locked: false, - }); - setIsBrushDown(true); + if (selectedToolSettings.type === "add") { + const brushPosition = getBrushPosition(); + setNoteData({ + x: brushPosition.x, + y: brushPosition.y, + size: defaultNoteSize, + text: "", + id: shortid.generate(), + lastModified: Date.now(), + lastModifiedBy: userId, + visible: true, + locked: false, + color: "yellow", + }); + setIsBrushDown(true); + } } function handleBrushMove() { - const brushPosition = getBrushPosition(); - setNoteData((prev) => ({ - ...prev, - x: brushPosition.x, - y: brushPosition.y, - })); - setIsBrushDown(true); + if (selectedToolSettings.type === "add") { + const brushPosition = getBrushPosition(); + setNoteData((prev) => ({ + ...prev, + x: brushPosition.x, + y: brushPosition.y, + })); + setIsBrushDown(true); + } } function handleBrushUp() { - onNoteAdd(noteData); + if (selectedToolSettings.type === "add") { + onNoteAdd(noteData); + onNoteMenuOpen(noteData.id, creatingNoteRef.current); + } setNoteData(null); setIsBrushDown(false); } @@ -91,9 +103,20 @@ function MapNotes({ return ( {notes.map((note) => ( - + ))} - {isBrushDown && noteData && } + + {isBrushDown && noteData && ( + + )} + ); } diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 98c992a..6090a72 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -167,7 +167,7 @@ function NetworkedMapAndTokens({ session }) { session.send("mapFogIndex", index); } - function handleNoteAdd(note) { + function handleNoteChange(note) { setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: { @@ -175,7 +175,7 @@ function NetworkedMapAndTokens({ session }) { [note.id]: note, }, })); - session.send("mapNoteAdd", note); + session.send("mapNoteChange", note); } /** @@ -406,7 +406,7 @@ function NetworkedMapAndTokens({ session }) { fogDrawActionIndex: data, })); } - if (id === "mapNoteAdd" && currentMapState) { + if (id === "mapNoteChange" && currentMapState) { setCurrentMapState((prevMapState) => ({ ...prevMapState, notes: { @@ -480,7 +480,7 @@ function NetworkedMapAndTokens({ session }) { onFogDraw={handleFogDraw} onFogDrawUndo={handleFogDrawUndo} onFogDrawRedo={handleFogDrawRedo} - onMapNoteAdd={handleNoteAdd} + onMapNoteChange={handleNoteChange} allowMapDrawing={canEditMapDrawing} allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap} From dcdbb1ea98a7df02d37179d8f4482a95d9daf9fa Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 12:28:28 +1100 Subject: [PATCH 009/147] Removed modes from note tool --- src/components/map/Map.js | 1 + src/components/map/MapControls.js | 2 - src/components/map/MapInteraction.js | 2 +- src/components/map/MapNote.js | 21 +++++++- src/components/map/MapNotes.js | 52 +++++++++---------- .../map/controls/NoteToolSettings.js | 47 ----------------- src/settings.js | 5 -- 7 files changed, 46 insertions(+), 84 deletions(-) delete mode 100644 src/components/map/controls/NoteToolSettings.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 710813f..0bca52e 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -370,6 +370,7 @@ function Map({ // TODO: Sort by last modified notes={mapState ? Object.values(mapState.notes) : []} onNoteMenuOpen={handleNoteMenuOpen} + draggable={selectedToolId === "note" || selectedToolId === "pan"} /> ); diff --git a/src/components/map/MapControls.js b/src/components/map/MapControls.js index 80e4840..fb71b72 100644 --- a/src/components/map/MapControls.js +++ b/src/components/map/MapControls.js @@ -9,7 +9,6 @@ import SelectMapButton from "./SelectMapButton"; import FogToolSettings from "./controls/FogToolSettings"; import DrawingToolSettings from "./controls/DrawingToolSettings"; import MeasureToolSettings from "./controls/MeasureToolSettings"; -import NoteToolSettings from "./controls/NoteToolSettings"; import PanToolIcon from "../../icons/PanToolIcon"; import FogToolIcon from "../../icons/FogToolIcon"; @@ -72,7 +71,6 @@ function MapContols({ id: "note", icon: , title: "Note Tool (N)", - SettingsComponent: NoteToolSettings, }, }; const tools = ["pan", "fog", "drawing", "measure", "pointer", "note"]; diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index 6be1a86..13e40d7 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -156,12 +156,12 @@ function MapInteraction({ return "move"; case "fog": case "drawing": - case "note": return settings.settings[tool].type === "move" ? "pointer" : "crosshair"; case "measure": case "pointer": + case "note": return "crosshair"; default: return "default"; diff --git a/src/components/map/MapNote.js b/src/components/map/MapNote.js index b2f5fba..0afea46 100644 --- a/src/components/map/MapNote.js +++ b/src/components/map/MapNote.js @@ -12,7 +12,9 @@ const textPadding = 4; function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { const { userId } = useContext(AuthContext); - const { mapWidth, mapHeight } = useContext(MapInteractionContext); + const { mapWidth, mapHeight, setPreventMapInteraction } = useContext( + MapInteractionContext + ); const noteWidth = map && (mapWidth / map.grid.size.x) * note.size; const noteHeight = map && (mapHeight / map.grid.size.y) * note.size; @@ -72,6 +74,18 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { }); } + function handlePointerDown(event) { + if (draggable) { + setPreventMapInteraction(true); + } + } + + function handlePointerUp(event) { + if (draggable) { + setPreventMapInteraction(false); + } + } + const [fontSize, setFontSize] = useState(1); useEffect(() => { const text = textRef.current; @@ -111,6 +125,10 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { draggable={draggable} onDragEnd={handleDragEnd} onDragMove={handleDragMove} + onMouseDown={handlePointerDown} + onMouseUp={handlePointerUp} + onTouchStart={handlePointerDown} + onTouchEnd={handlePointerUp} > {/* Use an invisible text block to work out text sizing */} diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index 64b1591..d297cb3 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -15,13 +15,13 @@ const defaultNoteSize = 2; function MapNotes({ map, - selectedToolSettings, active, gridSize, onNoteAdd, onNoteChange, notes, onNoteMenuOpen, + draggable, }) { const { interactionEmitter } = useContext(MapInteractionContext); const { userId } = useContext(AuthContext); @@ -50,38 +50,34 @@ function MapNotes({ } function handleBrushDown() { - if (selectedToolSettings.type === "add") { - const brushPosition = getBrushPosition(); - setNoteData({ - x: brushPosition.x, - y: brushPosition.y, - size: defaultNoteSize, - text: "", - id: shortid.generate(), - lastModified: Date.now(), - lastModifiedBy: userId, - visible: true, - locked: false, - color: "yellow", - }); - setIsBrushDown(true); - } + const brushPosition = getBrushPosition(); + setNoteData({ + x: brushPosition.x, + y: brushPosition.y, + size: defaultNoteSize, + text: "", + id: shortid.generate(), + lastModified: Date.now(), + lastModifiedBy: userId, + visible: true, + locked: false, + color: "yellow", + }); + setIsBrushDown(true); } function handleBrushMove() { - if (selectedToolSettings.type === "add") { - const brushPosition = getBrushPosition(); - setNoteData((prev) => ({ - ...prev, - x: brushPosition.x, - y: brushPosition.y, - })); - setIsBrushDown(true); - } + const brushPosition = getBrushPosition(); + setNoteData((prev) => ({ + ...prev, + x: brushPosition.x, + y: brushPosition.y, + })); + setIsBrushDown(true); } function handleBrushUp() { - if (selectedToolSettings.type === "add") { + if (noteData) { onNoteAdd(noteData); onNoteMenuOpen(noteData.id, creatingNoteRef.current); } @@ -108,7 +104,7 @@ function MapNotes({ map={map} key={note.id} onNoteMenuOpen={onNoteMenuOpen} - draggable={active && selectedToolSettings.type === "move"} + draggable={draggable} onNoteChange={onNoteChange} /> ))} diff --git a/src/components/map/controls/NoteToolSettings.js b/src/components/map/controls/NoteToolSettings.js deleted file mode 100644 index 869fd01..0000000 --- a/src/components/map/controls/NoteToolSettings.js +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; -import { Flex } from "theme-ui"; - -import ToolSection from "./ToolSection"; -import NoteAddIcon from "../../../icons/NoteAddIcon"; -import MoveIcon from "../../../icons/MoveIcon"; - -import useKeyboard from "../../../helpers/useKeyboard"; - -function NoteToolSettings({ settings, onSettingChange }) { - // Keyboard shortcuts - function handleKeyDown({ key }) { - if (key === "a") { - onSettingChange({ type: "add" }); - } else if (key === "v") { - onSettingChange({ type: "move" }); - } - } - - useKeyboard(handleKeyDown); - - const tools = [ - { - id: "add", - title: "Add Note (A)", - isSelected: settings.type === "add", - icon: , - }, - { - id: "move", - title: "Move Note (V)", - isSelected: settings.type === "move", - icon: , - }, - ]; - - return ( - - onSettingChange({ type: tool.id })} - /> - - ); -} - -export default NoteToolSettings; diff --git a/src/settings.js b/src/settings.js index e865967..16bfa7c 100644 --- a/src/settings.js +++ b/src/settings.js @@ -32,11 +32,6 @@ function loadVersions(settings) { ...prev, map: { fullScreen: false, labelSize: 1 }, })); - // v1.7.0 - Added note tool - settings.version(3, (prev) => ({ - ...prev, - note: { type: "add" }, - })); } export function getSettings() { From 807a1be626690d151682e3c35c88917ec25212ac Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 13:40:50 +1100 Subject: [PATCH 010/147] Removed label from map note menu --- src/components/map/MapNoteMenu.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/map/MapNoteMenu.js b/src/components/map/MapNoteMenu.js index d02afec..7781b92 100644 --- a/src/components/map/MapNoteMenu.js +++ b/src/components/map/MapNoteMenu.js @@ -113,14 +113,6 @@ function MapNoteMenu({ }} sx={{ alignItems: "center" }} > - - Label: - Date: Thu, 5 Nov 2020 14:41:33 +1100 Subject: [PATCH 011/147] Added note removal --- src/components/DragOverlay.js | 86 +++++++++++ src/components/map/Map.js | 48 ++++-- src/components/map/MapNotes.js | 10 +- .../{map/MapNote.js => note/Note.js} | 23 ++- src/components/note/NoteDragOverlay.js | 19 +++ .../{map/MapNoteMenu.js => note/NoteMenu.js} | 4 +- src/components/token/TokenDragOverlay.js | 137 ++++-------------- src/network/NetworkedMapAndTokens.js | 15 ++ 8 files changed, 213 insertions(+), 129 deletions(-) create mode 100644 src/components/DragOverlay.js rename src/components/{map/MapNote.js => note/Note.js} (91%) create mode 100644 src/components/note/NoteDragOverlay.js rename src/components/{map/MapNoteMenu.js => note/NoteMenu.js} (99%) diff --git a/src/components/DragOverlay.js b/src/components/DragOverlay.js new file mode 100644 index 0000000..18678e7 --- /dev/null +++ b/src/components/DragOverlay.js @@ -0,0 +1,86 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Box, IconButton } from "theme-ui"; + +import RemoveTokenIcon from "../icons/RemoveTokenIcon"; + +function DragOverlay({ dragging, node, onRemove }) { + const [isRemoveHovered, setIsRemoveHovered] = useState(false); + const removeTokenRef = useRef(); + + // Detect token hover on remove icon manually to support touch devices + useEffect(() => { + const map = document.querySelector(".map"); + const mapRect = map.getBoundingClientRect(); + + function detectRemoveHover() { + if (!node || !dragging || !removeTokenRef.current) { + return; + } + + const pointerPosition = node.getStage().getPointerPosition(); + const screenSpacePointerPosition = { + x: pointerPosition.x + mapRect.left, + y: pointerPosition.y + mapRect.top, + }; + const removeIconPosition = removeTokenRef.current.getBoundingClientRect(); + + if ( + screenSpacePointerPosition.x > removeIconPosition.left && + screenSpacePointerPosition.y > removeIconPosition.top && + screenSpacePointerPosition.x < removeIconPosition.right && + screenSpacePointerPosition.y < removeIconPosition.bottom + ) { + if (!isRemoveHovered) { + setIsRemoveHovered(true); + } + } else if (isRemoveHovered) { + setIsRemoveHovered(false); + } + } + + let handler; + if (node && dragging) { + handler = setInterval(detectRemoveHover, 100); + } + + return () => { + if (handler) { + clearInterval(handler); + } + }; + }, [isRemoveHovered, dragging, node]); + + // Detect drag end of token image and remove it if it is over the remove icon + useEffect(() => { + if (!dragging && node && isRemoveHovered) { + onRemove(); + } + }); + + return ( + dragging && ( + + + + + + ) + ); +} + +export default DragOverlay; diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 0bca52e..29467db 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -17,9 +17,10 @@ import SettingsContext from "../../contexts/SettingsContext"; import TokenMenu from "../token/TokenMenu"; import TokenDragOverlay from "../token/TokenDragOverlay"; +import NoteMenu from "../note/NoteMenu"; +import NoteDragOverlay from "../note/NoteDragOverlay"; import { drawActionsToShapes } from "../../helpers/drawing"; -import MapNoteMenu from "./MapNoteMenu"; function Map({ map, @@ -35,6 +36,7 @@ function Map({ onFogDrawUndo, onFogDrawRedo, onMapNoteChange, + onMapNoteRemove, allowMapDrawing, allowFogDrawing, allowMapChange, @@ -186,7 +188,7 @@ function Map({ const [isTokenMenuOpen, setIsTokenMenuOpen] = useState(false); const [tokenMenuOptions, setTokenMenuOptions] = useState({}); - const [draggingTokenOptions, setDraggingTokenOptions] = useState(); + const [tokenDraggingOptions, setTokenDraggingOptions] = useState(); function handleTokenMenuOpen(tokenStateId, tokenImage) { setTokenMenuOptions({ tokenStateId, tokenImage }); setIsTokenMenuOpen(true); @@ -245,7 +247,7 @@ function Map({ const mapTokens = map && mapState && ( {Object.values(mapState.tokens) - .sort((a, b) => sortMapTokenStates(a, b, draggingTokenOptions)) + .sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions)) .map((tokenState) => ( - setDraggingTokenOptions({ + setTokenDraggingOptions({ dragging: true, tokenState, tokenGroup: e.target, }) } onTokenDragEnd={() => - setDraggingTokenOptions({ - ...draggingTokenOptions, + setTokenDraggingOptions({ + ...tokenDraggingOptions, dragging: false, }) } @@ -291,17 +293,17 @@ function Map({ /> ); - const tokenDragOverlay = draggingTokenOptions && ( + const tokenDragOverlay = tokenDraggingOptions && ( { onMapTokenStateRemove(state); - setDraggingTokenOptions(null); + setTokenDraggingOptions(null); }} onTokenStateChange={onMapTokenStateChange} - tokenState={draggingTokenOptions && draggingTokenOptions.tokenState} - tokenGroup={draggingTokenOptions && draggingTokenOptions.tokenGroup} - dragging={draggingTokenOptions && draggingTokenOptions.dragging} - token={tokensById[draggingTokenOptions.tokenState.tokenId]} + tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState} + tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup} + dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)} + token={tokensById[tokenDraggingOptions.tokenState.tokenId]} mapState={mapState} /> ); @@ -354,6 +356,7 @@ function Map({ const [isNoteMenuOpen, setIsNoteMenuOpen] = useState(false); const [noteMenuOptions, setNoteMenuOptions] = useState({}); + const [noteDraggingOptions, setNoteDraggingOptions] = useState(); function handleNoteMenuOpen(noteId, noteNode) { setNoteMenuOptions({ noteId, noteNode }); setIsNoteMenuOpen(true); @@ -371,11 +374,17 @@ function Map({ notes={mapState ? Object.values(mapState.notes) : []} onNoteMenuOpen={handleNoteMenuOpen} draggable={selectedToolId === "note" || selectedToolId === "pan"} + onNoteDragStart={(e, noteId) => + setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) + } + onNoteDragEnd={() => + setNoteDraggingOptions({ ...noteDraggingOptions, dragging: false }) + } /> ); const noteMenu = ( - setIsNoteMenuOpen(false)} onNoteChange={onMapNoteChange} @@ -385,6 +394,18 @@ function Map({ /> ); + const noteDragOverlay = ( + { + onMapNoteRemove(noteId); + setNoteDraggingOptions(null); + }} + /> + ); + return ( } diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index d297cb3..fb81e21 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -9,7 +9,7 @@ import AuthContext from "../../contexts/AuthContext"; import { getBrushPositionForTool } from "../../helpers/drawing"; import { getRelativePointerPositionNormalized } from "../../helpers/konva"; -import MapNote from "./MapNote"; +import Note from "../note/Note"; const defaultNoteSize = 2; @@ -22,6 +22,8 @@ function MapNotes({ notes, onNoteMenuOpen, draggable, + onNoteDragStart, + onNoteDragEnd, }) { const { interactionEmitter } = useContext(MapInteractionContext); const { userId } = useContext(AuthContext); @@ -99,18 +101,20 @@ function MapNotes({ return ( {notes.map((note) => ( - ))} {isBrushDown && noteData && ( - + )} diff --git a/src/components/map/MapNote.js b/src/components/note/Note.js similarity index 91% rename from src/components/map/MapNote.js rename to src/components/note/Note.js index 0afea46..2e7de59 100644 --- a/src/components/map/MapNote.js +++ b/src/components/note/Note.js @@ -10,7 +10,15 @@ import colors from "../../helpers/colors"; const snappingThreshold = 1 / 5; const textPadding = 4; -function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { +function Note({ + note, + map, + onNoteChange, + onNoteMenuOpen, + draggable, + onNoteDragStart, + onNoteDragEnd, +}) { const { userId } = useContext(AuthContext); const { mapWidth, mapHeight, setPreventMapInteraction } = useContext( MapInteractionContext @@ -26,6 +34,10 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { } } + function handleDragStart(event) { + onNoteDragStart && onNoteDragStart(event, note.id); + } + function handleDragMove(event) { const noteGroup = event.target; // Snap to corners of grid @@ -72,15 +84,17 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { lastModifiedBy: userId, lastModified: Date.now(), }); + onNoteDragEnd && onNoteDragEnd(note.id); + setPreventMapInteraction(false); } - function handlePointerDown(event) { + function handlePointerDown() { if (draggable) { setPreventMapInteraction(true); } } - function handlePointerUp(event) { + function handlePointerUp() { if (draggable) { setPreventMapInteraction(false); } @@ -123,6 +137,7 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { offsetX={noteWidth / 2} offsetY={noteHeight / 2} draggable={draggable} + onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragMove={handleDragMove} onMouseDown={handlePointerDown} @@ -157,4 +172,4 @@ function MapNote({ note, map, onNoteChange, onNoteMenuOpen, draggable }) { ); } -export default MapNote; +export default Note; diff --git a/src/components/note/NoteDragOverlay.js b/src/components/note/NoteDragOverlay.js new file mode 100644 index 0000000..9df0713 --- /dev/null +++ b/src/components/note/NoteDragOverlay.js @@ -0,0 +1,19 @@ +import React from "react"; + +import DragOverlay from "../DragOverlay"; + +function NoteDragOverlay({ onNoteRemove, noteId, noteGroup, dragging }) { + function handleNoteRemove() { + onNoteRemove(noteId); + } + + return ( + + ); +} + +export default NoteDragOverlay; diff --git a/src/components/map/MapNoteMenu.js b/src/components/note/NoteMenu.js similarity index 99% rename from src/components/map/MapNoteMenu.js rename to src/components/note/NoteMenu.js index 7781b92..783025d 100644 --- a/src/components/map/MapNoteMenu.js +++ b/src/components/note/NoteMenu.js @@ -16,7 +16,7 @@ import AuthContext from "../../contexts/AuthContext"; const defaultNoteMaxSize = 6; -function MapNoteMenu({ +function NoteMenu({ isOpen, onRequestClose, note, @@ -205,4 +205,4 @@ function MapNoteMenu({ ); } -export default MapNoteMenu; +export default NoteMenu; diff --git a/src/components/token/TokenDragOverlay.js b/src/components/token/TokenDragOverlay.js index 68b9d29..4d90e9a 100644 --- a/src/components/token/TokenDragOverlay.js +++ b/src/components/token/TokenDragOverlay.js @@ -1,11 +1,10 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; -import { Box, IconButton } from "theme-ui"; - -import RemoveTokenIcon from "../../icons/RemoveTokenIcon"; +import React, { useContext } from "react"; import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; +import DragOverlay from "../DragOverlay"; + function TokenDragOverlay({ onTokenStateRemove, onTokenStateChange, @@ -16,114 +15,38 @@ function TokenDragOverlay({ mapState, }) { const { userId } = useContext(AuthContext); - const { setPreventMapInteraction, mapWidth, mapHeight } = useContext( - MapInteractionContext - ); + const { mapWidth, mapHeight } = useContext(MapInteractionContext); - const [isRemoveHovered, setIsRemoveHovered] = useState(false); - const removeTokenRef = useRef(); - - // Detect token hover on remove icon manually to support touch devices - useEffect(() => { - const map = document.querySelector(".map"); - const mapRect = map.getBoundingClientRect(); - - function detectRemoveHover() { - if (!tokenGroup) { - return; - } - - const pointerPosition = tokenGroup.getStage().getPointerPosition(); - const screenSpacePointerPosition = { - x: pointerPosition.x + mapRect.left, - y: pointerPosition.y + mapRect.top, - }; - if (!removeTokenRef.current) { - return; - } - const removeIconPosition = removeTokenRef.current.getBoundingClientRect(); - - if ( - screenSpacePointerPosition.x > removeIconPosition.left && - screenSpacePointerPosition.y > removeIconPosition.top && - screenSpacePointerPosition.x < removeIconPosition.right && - screenSpacePointerPosition.y < removeIconPosition.bottom - ) { - if (!isRemoveHovered) { - setIsRemoveHovered(true); - } - } else if (isRemoveHovered) { - setIsRemoveHovered(false); + function handleTokenRemove() { + // Handle other tokens when a vehicle gets deleted + if (token && token.category === "vehicle") { + const layer = tokenGroup.getLayer(); + const mountedTokens = tokenGroup.find(".token"); + for (let mountedToken of mountedTokens) { + // Save and restore token position after moving layer + const position = mountedToken.absolutePosition(); + mountedToken.moveTo(layer); + mountedToken.absolutePosition(position); + onTokenStateChange({ + [mountedToken.id()]: { + ...mapState.tokens[mountedToken.id()], + x: mountedToken.x() / mapWidth, + y: mountedToken.y() / mapHeight, + lastModifiedBy: userId, + lastModified: Date.now(), + }, + }); } } - - let handler; - if (tokenState && tokenGroup && dragging) { - handler = setInterval(detectRemoveHover, 100); - } - - return () => { - if (handler) { - clearInterval(handler); - } - }; - }, [tokenState, tokenGroup, isRemoveHovered, dragging]); - - // Detect drag end of token image and remove it if it is over the remove icon - useEffect(() => { - function handleTokenDragEnd() { - // Handle other tokens when a vehicle gets deleted - if (token && token.category === "vehicle") { - const layer = tokenGroup.getLayer(); - const mountedTokens = tokenGroup.find(".token"); - for (let mountedToken of mountedTokens) { - // Save and restore token position after moving layer - const position = mountedToken.absolutePosition(); - mountedToken.moveTo(layer); - mountedToken.absolutePosition(position); - onTokenStateChange({ - [mountedToken.id()]: { - ...mapState.tokens[mountedToken.id()], - x: mountedToken.x() / mapWidth, - y: mountedToken.y() / mapHeight, - lastModifiedBy: userId, - lastModified: Date.now(), - }, - }); - } - } - onTokenStateRemove(tokenState); - setPreventMapInteraction(false); - } - - if (!dragging && tokenState && isRemoveHovered) { - handleTokenDragEnd(); - } - }); + onTokenStateRemove(tokenState); + } return ( - dragging && ( - - - - - - ) + ); } diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 6090a72..9d22be7 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -178,6 +178,14 @@ function NetworkedMapAndTokens({ session }) { session.send("mapNoteChange", note); } + function handleNoteRemove(noteId) { + setCurrentMapState((prevMapState) => ({ + ...prevMapState, + notes: omit(prevMapState.notes, [noteId]), + })); + session.send("mapNoteRemove", noteId); + } + /** * Token state */ @@ -415,6 +423,12 @@ function NetworkedMapAndTokens({ session }) { }, })); } + if (id === "mapNoteRemove" && currentMapState) { + setCurrentMapState((prevMapState) => ({ + ...prevMapState, + notes: omit(prevMapState.notes, [data]), + })); + } } function handlePeerDataProgress({ id, total, count }) { @@ -481,6 +495,7 @@ function NetworkedMapAndTokens({ session }) { onFogDrawUndo={handleFogDrawUndo} onFogDrawRedo={handleFogDrawRedo} onMapNoteChange={handleNoteChange} + onMapNoteRemove={handleNoteRemove} allowMapDrawing={canEditMapDrawing} allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap} From 5fd3f6d8143f9dbc520c449ca3a298dec02a8691 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 14:48:32 +1100 Subject: [PATCH 012/147] Changed note text color based off of background color --- src/components/note/Note.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/note/Note.js b/src/components/note/Note.js index 2e7de59..3c4719e 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -156,7 +156,11 @@ function Note({ /> Date: Thu, 5 Nov 2020 15:17:23 +1100 Subject: [PATCH 013/147] Added animation and visibility to notes --- src/components/note/Note.js | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/components/note/Note.js b/src/components/note/Note.js index 3c4719e..628dd57 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -1,11 +1,13 @@ import React, { useContext, useEffect, useState, useRef } from "react"; -import { Group, Rect, Text } from "react-konva"; +import { Rect, Text } from "react-konva"; +import { useSpring, animated } from "react-spring/konva"; import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; import * as Vector2 from "../../helpers/vector2"; import colors from "../../helpers/colors"; +import usePrevious from "../../helpers/usePrevious"; const snappingThreshold = 1 / 5; const textPadding = 4; @@ -103,6 +105,11 @@ function Note({ const [fontSize, setFontSize] = useState(1); useEffect(() => { const text = textRef.current; + + if (!text) { + return; + } + function findFontSize() { // Create an array from 4 to 18 scaled to the note size const sizes = Array.from( @@ -120,18 +127,34 @@ function Note({ } }); } - setFontSize(findFontSize()); }, [note, noteWidth]); const textRef = useRef(); + // Animate to new note positions if edited by others + const noteX = note.x * mapWidth; + const noteY = note.y * mapHeight; + const previousWidth = usePrevious(mapWidth); + const previousHeight = usePrevious(mapHeight); + const resized = mapWidth !== previousWidth || mapHeight !== previousHeight; + const skipAnimation = note.lastModifiedBy === userId || resized; + const props = useSpring({ + x: noteX, + y: noteY, + immediate: skipAnimation, + }); + + // When a note is hidden if you aren't the map owner hide it completely + if (map && !note.visible && map.owner !== userId) { + return null; + } + return ( - {/* Use an invisible text block to work out text sizing */} - + ); } From f5aec514f873ef93a61b703a16fd4827644fbfc0 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 15:30:22 +1100 Subject: [PATCH 014/147] Added locking to notes --- src/components/map/MapNotes.js | 2 +- src/components/note/Note.js | 35 ++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index fb81e21..4efacf9 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -106,7 +106,7 @@ function MapNotes({ map={map} key={note.id} onNoteMenuOpen={onNoteMenuOpen} - draggable={draggable} + draggable={draggable && !note.locked} onNoteChange={onNoteChange} onNoteDragStart={onNoteDragStart} onNoteDragEnd={onNoteDragEnd} diff --git a/src/components/note/Note.js b/src/components/note/Note.js index 628dd57..51f9c1c 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -29,13 +29,6 @@ function Note({ const noteWidth = map && (mapWidth / map.grid.size.x) * note.size; const noteHeight = map && (mapHeight / map.grid.size.y) * note.size; - function handleClick(event) { - if (draggable) { - const noteNode = event.target; - onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode); - } - } - function handleDragStart(event) { onNoteDragStart && onNoteDragStart(event, note.id); } @@ -90,16 +83,38 @@ function Note({ setPreventMapInteraction(false); } - function handlePointerDown() { + function handleClick(event) { if (draggable) { - setPreventMapInteraction(true); + const noteNode = event.target; + onNoteMenuOpen && onNoteMenuOpen(note.id, noteNode); } } - function handlePointerUp() { + // Store note pointer down time to check for a click when note is locked + const notePointerDownTimeRef = useRef(); + function handlePointerDown(event) { + if (draggable) { + setPreventMapInteraction(true); + } + if (note.locked && map.owner === userId) { + notePointerDownTimeRef.current = event.evt.timeStamp; + } + } + + function handlePointerUp(event) { if (draggable) { setPreventMapInteraction(false); } + // 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 down and up time is small trigger a click + const delta = event.evt.timeStamp - notePointerDownTimeRef.current; + if (delta < 300) { + const noteNode = event.target; + onNoteMenuOpen(note.id, noteNode); + } + } } const [fontSize, setFontSize] = useState(1); From 8873e592055d09b64be7faeb35ed8027604f9026 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 15:39:56 +1100 Subject: [PATCH 015/147] Added sorting to notes --- src/components/map/Map.js | 44 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 29467db..adc9029 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -208,7 +208,7 @@ function Map({ } // Sort so vehicles render below other tokens - function sortMapTokenStates(a, b, draggingTokenOptions) { + function sortMapTokenStates(a, b, tokenDraggingOptions) { const tokenA = tokensById[a.tokenId]; const tokenB = tokensById[b.tokenId]; if (tokenA && tokenB) { @@ -218,16 +218,16 @@ function Map({ const bWeight = getMapTokenCategoryWeight(tokenB.category); return bWeight - aWeight; } else if ( - draggingTokenOptions && - draggingTokenOptions.dragging && - draggingTokenOptions.tokenState.id === a.id + tokenDraggingOptions && + tokenDraggingOptions.dragging && + tokenDraggingOptions.tokenState.id === a.id ) { // If dragging token a move above return 1; } else if ( - draggingTokenOptions && - draggingTokenOptions.dragging && - draggingTokenOptions.tokenState.id === b.id + tokenDraggingOptions && + tokenDraggingOptions.dragging && + tokenDraggingOptions.tokenState.id === b.id ) { // If dragging token b move above return -1; @@ -362,6 +362,27 @@ function Map({ setIsNoteMenuOpen(true); } + function sortNotes(a, b, noteDraggingOptions) { + if ( + noteDraggingOptions && + noteDraggingOptions.dragging && + noteDraggingOptions.noteId === a.id + ) { + // If dragging token `a` move above + return 1; + } else if ( + noteDraggingOptions && + noteDraggingOptions.dragging && + noteDraggingOptions.noteId === b.id + ) { + // If dragging token `b` move above + return -1; + } else { + // Else sort so last modified is on top + return a.lastModified - b.lastModified; + } + } + const mapNotes = ( + sortNotes(a, b, noteDraggingOptions) + ) + : [] + } onNoteMenuOpen={handleNoteMenuOpen} draggable={selectedToolId === "note" || selectedToolId === "pan"} onNoteDragStart={(e, noteId) => From 6fedcc171d80cefe4c3c89910b7f090953786c6b Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 16:21:52 +1100 Subject: [PATCH 016/147] Added note edit flags --- src/components/map/Map.js | 10 ++++++++-- src/components/map/MapSettings.js | 10 ++++++++++ src/contexts/MapDataContext.js | 2 +- src/database.js | 1 + src/network/NetworkedMapAndTokens.js | 7 +++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/components/map/Map.js b/src/components/map/Map.js index adc9029..cd3305f 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -40,6 +40,7 @@ function Map({ allowMapDrawing, allowFogDrawing, allowMapChange, + allowNoteEditing, disabledTokens, session, }) { @@ -138,7 +139,6 @@ function Map({ disabledControls.push("pan"); disabledControls.push("measure"); disabledControls.push("pointer"); - disabledControls.push("note"); } if (!allowFogDrawing) { disabledControls.push("fog"); @@ -146,6 +146,9 @@ function Map({ if (!allowMapChange) { disabledControls.push("map"); } + if (!allowNoteEditing) { + disabledControls.push("note"); + } const disabledSettings = { fog: [], drawing: [] }; if (mapShapes.length === 0) { @@ -399,7 +402,10 @@ function Map({ : [] } onNoteMenuOpen={handleNoteMenuOpen} - draggable={selectedToolId === "note" || selectedToolId === "pan"} + draggable={ + allowNoteEditing && + (selectedToolId === "note" || selectedToolId === "pan") + } onNoteDragStart={(e, noteId) => setNoteDraggingOptions({ dragging: true, noteId, noteGroup: e.target }) } diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js index 3c6181b..622bd0d 100644 --- a/src/components/map/MapSettings.js +++ b/src/components/map/MapSettings.js @@ -233,6 +233,16 @@ function MapSettings({ /> Tokens + diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index a9a3621..9d7e4b4 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -19,7 +19,7 @@ const defaultMapState = { fogDrawActionIndex: -1, fogDrawActions: [], // Flags to determine what other people can edit - editFlags: ["drawing", "tokens"], + editFlags: ["drawing", "tokens", "notes"], notes: {}, }; diff --git a/src/database.js b/src/database.js index 91cf035..6164f28 100644 --- a/src/database.js +++ b/src/database.js @@ -277,6 +277,7 @@ function loadVersions(db) { .toCollection() .modify((state) => { state.notes = {}; + state.editFlags = [...state.editFlags, "notes"]; }); }); } diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 9d22be7..8566c54 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -463,6 +463,12 @@ function NetworkedMapAndTokens({ session }) { currentMapState !== null && (currentMapState.editFlags.includes("fog") || currentMap.owner === userId); + const canEditNotes = + currentMap !== null && + currentMapState !== null && + (currentMapState.editFlags.includes("notes") || + currentMap.owner === userId); + const disabledMapTokens = {}; // If we have a map and state and have the token permission disabled // and are not the map owner @@ -499,6 +505,7 @@ function NetworkedMapAndTokens({ session }) { allowMapDrawing={canEditMapDrawing} allowFogDrawing={canEditFogDrawing} allowMapChange={canChangeMap} + allowNoteEditing={canEditNotes} disabledTokens={disabledMapTokens} session={session} /> From 78aba1a83fc31560be0c87f7a0efdedb6630869e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 16:50:43 +1100 Subject: [PATCH 017/147] Added max space size to build script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e073e3b..2cd8584 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "start": "react-scripts start", - "build": "react-scripts build", + "build": "react-scripts --max_old_space_size=4096 build", "test": "react-scripts test", "eject": "react-scripts eject" }, From 61a4596a3d5b9a004f5d528f017ede77260a3fca Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 16:58:09 +1100 Subject: [PATCH 018/147] Removed unused import --- src/network/Connection.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/network/Connection.js b/src/network/Connection.js index daf312c..40c490d 100644 --- a/src/network/Connection.js +++ b/src/network/Connection.js @@ -3,7 +3,6 @@ import { encode, decode } from "@msgpack/msgpack"; import shortid from "shortid"; import blobToBuffer from "../helpers/blobToBuffer"; -import { logError } from "../helpers/logging"; // Limit buffer size to 16kb to avoid issues with chrome packet size // http://viblast.com/blog/2015/2/5/webrtc-data-channel-message-size/ From 395c6d3b1505c32f84d25ec4f35a377f6a582047 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 5 Nov 2020 16:58:48 +1100 Subject: [PATCH 019/147] Updated version number to v1.7.0 preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2cd8584..92948f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "owlbear-rodeo", - "version": "1.6.2", + "version": "1.7.0 (preview)", "private": true, "dependencies": { "@babylonjs/core": "^4.1.0", From 088466ea074d38a1451f73189710827568f6bee3 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 6 Nov 2020 13:35:11 +1100 Subject: [PATCH 020/147] Added slider label on drag --- src/components/Slider.js | 69 +++++++++++++++++++++++++++++++ src/components/note/NoteMenu.js | 4 +- src/components/party/Stream.js | 3 +- src/components/token/TokenMenu.js | 4 +- src/modals/SettingsModal.js | 12 ++---- 5 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 src/components/Slider.js diff --git a/src/components/Slider.js b/src/components/Slider.js new file mode 100644 index 0000000..fdc601b --- /dev/null +++ b/src/components/Slider.js @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import { Box, Slider as ThemeSlider } from "theme-ui"; + +function Slider({ min, max, value, ml, mr, labelFunc, ...rest }) { + const percentValue = ((value - min) * 100) / (max - min); + + const [labelVisible, setLabelVisible] = useState(false); + + return ( + + {labelVisible && ( + + + + {labelFunc(value)} + + + + )} + setLabelVisible(true)} + onMouseUp={() => setLabelVisible(false)} + onTouchStart={() => setLabelVisible(true)} + onTouchEnd={() => setLabelVisible(false)} + {...rest} + /> + + ); +} + +Slider.defaultProps = { + min: 0, + max: 1, + value: 0, + ml: 0, + mr: 0, + labelFunc: (value) => value, +}; + +export default Slider; diff --git a/src/components/note/NoteMenu.js b/src/components/note/NoteMenu.js index 783025d..0fd0732 100644 --- a/src/components/note/NoteMenu.js +++ b/src/components/note/NoteMenu.js @@ -1,5 +1,7 @@ import React, { useEffect, useState, useContext } from "react"; -import { Box, Input, Slider, Flex, Text, IconButton } from "theme-ui"; +import { Box, Input, Flex, Text, IconButton } from "theme-ui"; + +import Slider from "../Slider"; import MapMenu from "../map/MapMenu"; diff --git a/src/components/party/Stream.js b/src/components/party/Stream.js index 809db3d..d3e7bcc 100644 --- a/src/components/party/Stream.js +++ b/src/components/party/Stream.js @@ -1,9 +1,10 @@ import React, { useState, useRef, useEffect } from "react"; -import { Text, IconButton, Box, Slider, Flex } from "theme-ui"; +import { Text, IconButton, Box, Flex } from "theme-ui"; import StreamMuteIcon from "../../icons/StreamMuteIcon"; import Banner from "../Banner"; +import Slider from "../Slider"; function Stream({ stream, nickname }) { const [streamVolume, setStreamVolume] = useState(1); diff --git a/src/components/token/TokenMenu.js b/src/components/token/TokenMenu.js index bf9679e..2009a17 100644 --- a/src/components/token/TokenMenu.js +++ b/src/components/token/TokenMenu.js @@ -1,5 +1,7 @@ import React, { useEffect, useState, useContext } from "react"; -import { Box, Input, Slider, Flex, Text, IconButton } from "theme-ui"; +import { Box, Input, Flex, Text, IconButton } from "theme-ui"; + +import Slider from "../Slider"; import MapMenu from "../map/MapMenu"; diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js index d1441de..126d371 100644 --- a/src/modals/SettingsModal.js +++ b/src/modals/SettingsModal.js @@ -1,15 +1,8 @@ import React, { useState, useContext } from "react"; -import { - Label, - Flex, - Button, - useColorMode, - Checkbox, - Slider, - Divider, -} from "theme-ui"; +import { Label, Flex, Button, useColorMode, Checkbox, Divider } from "theme-ui"; import Modal from "../components/Modal"; +import Slider from "../components/Slider"; import AuthContext from "../contexts/AuthContext"; import DatabaseContext from "../contexts/DatabaseContext"; @@ -86,6 +79,7 @@ function SettingsModal({ isOpen, onRequestClose }) { sx={{ width: "initial" }} value={labelSize} onChange={(e) => setLabelSize(parseFloat(e.target.value))} + labelFunc={(value) => `${value}x`} /> From cb2c432d0a60757ea54b70553721570b49c98a44 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 6 Nov 2020 15:48:35 +1100 Subject: [PATCH 021/147] Changed min token and note scale to 0.5 and fix grid snapping for non integer values --- src/components/map/MapToken.js | 32 +--------------- src/components/note/Note.js | 32 +--------------- src/components/note/NoteMenu.js | 6 +-- src/components/token/TokenMenu.js | 6 +-- src/helpers/map.js | 62 +++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 66 deletions(-) diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index d9dff3f..f6fd65d 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -6,7 +6,7 @@ import useImage from "use-image"; import useDataSource from "../../helpers/useDataSource"; import useDebounce from "../../helpers/useDebounce"; import usePrevious from "../../helpers/usePrevious"; -import * as Vector2 from "../../helpers/vector2"; +import { snapNodeToMap } from "../../helpers/map"; import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; @@ -82,35 +82,7 @@ function MapToken({ const tokenGroup = event.target; // Snap to corners of grid if (map.snapToGrid) { - const offset = Vector2.multiply(map.grid.inset.topLeft, { - x: mapWidth, - y: mapHeight, - }); - const position = { - x: tokenGroup.x() + tokenGroup.width() / 2, - y: tokenGroup.y() + tokenGroup.height() / 2, - }; - const gridSize = { - x: - (mapWidth * - (map.grid.inset.bottomRight.x - map.grid.inset.topLeft.x)) / - map.grid.size.x, - y: - (mapHeight * - (map.grid.inset.bottomRight.y - map.grid.inset.topLeft.y)) / - map.grid.size.y, - }; - // Transform into offset space, round, then transform back - const gridSnap = Vector2.add( - Vector2.roundTo(Vector2.subtract(position, offset), gridSize), - offset - ); - const gridDistance = Vector2.length(Vector2.subtract(gridSnap, position)); - const minGrid = Vector2.min(gridSize); - if (gridDistance < minGrid * snappingThreshold) { - tokenGroup.x(gridSnap.x - tokenGroup.width() / 2); - tokenGroup.y(gridSnap.y - tokenGroup.height() / 2); - } + snapNodeToMap(map, mapWidth, mapHeight, tokenGroup, snappingThreshold); } } diff --git a/src/components/note/Note.js b/src/components/note/Note.js index 51f9c1c..d11da63 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -5,7 +5,7 @@ import { useSpring, animated } from "react-spring/konva"; import AuthContext from "../../contexts/AuthContext"; import MapInteractionContext from "../../contexts/MapInteractionContext"; -import * as Vector2 from "../../helpers/vector2"; +import { snapNodeToMap } from "../../helpers/map"; import colors from "../../helpers/colors"; import usePrevious from "../../helpers/usePrevious"; @@ -37,35 +37,7 @@ function Note({ const noteGroup = event.target; // Snap to corners of grid if (map.snapToGrid) { - const offset = Vector2.multiply(map.grid.inset.topLeft, { - x: mapWidth, - y: mapHeight, - }); - const position = { - x: noteGroup.x() + noteGroup.width() / 2, - y: noteGroup.y() + noteGroup.height() / 2, - }; - const gridSize = { - x: - (mapWidth * - (map.grid.inset.bottomRight.x - map.grid.inset.topLeft.x)) / - map.grid.size.x, - y: - (mapHeight * - (map.grid.inset.bottomRight.y - map.grid.inset.topLeft.y)) / - map.grid.size.y, - }; - // Transform into offset space, round, then transform back - const gridSnap = Vector2.add( - Vector2.roundTo(Vector2.subtract(position, offset), gridSize), - offset - ); - const gridDistance = Vector2.length(Vector2.subtract(gridSnap, position)); - const minGrid = Vector2.min(gridSize); - if (gridDistance < minGrid * snappingThreshold) { - noteGroup.x(gridSnap.x - noteGroup.width() / 2); - noteGroup.y(gridSnap.y - noteGroup.height() / 2); - } + snapNodeToMap(map, mapWidth, mapHeight, noteGroup, snappingThreshold); } } diff --git a/src/components/note/NoteMenu.js b/src/components/note/NoteMenu.js index 0fd0732..6ea59cb 100644 --- a/src/components/note/NoteMenu.js +++ b/src/components/note/NoteMenu.js @@ -63,7 +63,7 @@ function NoteMenu({ } function handleSizeChange(event) { - const newSize = parseInt(event.target.value); + const newSize = parseFloat(event.target.value); note && onNoteChange({ ...note, size: newSize }); } @@ -177,8 +177,8 @@ function NoteMenu({ diff --git a/src/components/token/TokenMenu.js b/src/components/token/TokenMenu.js index 2009a17..645cbe2 100644 --- a/src/components/token/TokenMenu.js +++ b/src/components/token/TokenMenu.js @@ -72,7 +72,7 @@ function TokenMenu({ } function handleSizeChange(event) { - const newSize = parseInt(event.target.value); + const newSize = parseFloat(event.target.value); tokenState && onTokenStateChange({ [tokenState.id]: { ...tokenState, size: newSize } }); } @@ -211,8 +211,8 @@ function TokenMenu({ diff --git a/src/helpers/map.js b/src/helpers/map.js index 3ded592..a2101dc 100644 --- a/src/helpers/map.js +++ b/src/helpers/map.js @@ -1,4 +1,5 @@ import GridSizeModel from "../ml/gridSize/GridSizeModel"; +import * as Vector2 from "./vector2"; import { logError } from "./logging"; @@ -162,3 +163,64 @@ export function getMapMaxZoom(map) { // Return max grid size / 2 return Math.max(Math.min(map.grid.size.x, map.grid.size.y) / 2, 5); } + +export function snapNodeToMap( + map, + mapWidth, + mapHeight, + node, + snappingThreshold +) { + const offset = Vector2.multiply(map.grid.inset.topLeft, { + x: mapWidth, + y: mapHeight, + }); + const gridSize = { + x: + (mapWidth * (map.grid.inset.bottomRight.x - map.grid.inset.topLeft.x)) / + map.grid.size.x, + y: + (mapHeight * (map.grid.inset.bottomRight.y - map.grid.inset.topLeft.y)) / + map.grid.size.y, + }; + + const position = node.position(); + const halfSize = Vector2.divide({ x: node.width(), y: node.height() }, 2); + + // Offsets to tranform the centered position into the four corners + const cornerOffsets = [ + halfSize, + { x: -halfSize.x, y: -halfSize.y }, + { x: halfSize.x, y: -halfSize.y }, + { x: -halfSize.x, y: halfSize.y }, + ]; + + // Minimum distance from a corner to the grid + let minCornerGridDistance = Number.MAX_VALUE; + // Minimum component of the difference between the min corner and the grid + let minCornerMinComponent; + // Closest grid value + let minGridSnap; + + // Find the closest corner to the grid + for (let cornerOffset of cornerOffsets) { + const corner = Vector2.add(position, cornerOffset); + // Transform into offset space, round, then transform back + const gridSnap = Vector2.add( + Vector2.roundTo(Vector2.subtract(corner, offset), gridSize), + offset + ); + const gridDistance = Vector2.length(Vector2.subtract(gridSnap, corner)); + const minComponent = Vector2.min(gridSize); + if (gridDistance < minCornerGridDistance) { + minCornerGridDistance = gridDistance; + minCornerMinComponent = minComponent; + // Move the grid value back to the center + minGridSnap = Vector2.subtract(gridSnap, cornerOffset); + } + } + + if (minCornerGridDistance < minCornerMinComponent * snappingThreshold) { + node.position(minGridSnap); + } +} From 485c418b5789b66885f299cc2e9bd526ef56c10e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 6 Nov 2020 16:27:47 +1100 Subject: [PATCH 022/147] Updated note text sizes to be relative to the note height --- src/components/note/Note.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/note/Note.js b/src/components/note/Note.js index d11da63..884757e 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -10,7 +10,6 @@ import colors from "../../helpers/colors"; import usePrevious from "../../helpers/usePrevious"; const snappingThreshold = 1 / 5; -const textPadding = 4; function Note({ note, @@ -28,6 +27,7 @@ function Note({ const noteWidth = map && (mapWidth / map.grid.size.x) * note.size; const noteHeight = map && (mapHeight / map.grid.size.y) * note.size; + const notePadding = noteWidth / 10; function handleDragStart(event) { onNoteDragStart && onNoteDragStart(event, note.id); @@ -98,15 +98,15 @@ function Note({ } function findFontSize() { - // Create an array from 4 to 18 scaled to the note size + // Create an array from 1 / 10 of the note height to the full note height const sizes = Array.from( - { length: 14 * note.size }, - (_, i) => i + 4 * note.size + { length: Math.ceil(noteHeight - notePadding * 2) }, + (_, i) => i + Math.ceil(noteHeight / 10) ); return sizes.reduce((prev, curr) => { text.fontSize(curr); - const width = text.getTextWidth() + textPadding * 2; + const width = text.getTextWidth() + notePadding * 2; if (width < noteWidth) { return curr; } else { @@ -115,7 +115,7 @@ function Note({ }); } setFontSize(findFontSize()); - }, [note, noteWidth]); + }, [note, noteWidth, noteHeight, notePadding]); const textRef = useRef(); @@ -174,12 +174,11 @@ function Note({ } align="center" verticalAlign="middle" - padding={textPadding} + padding={notePadding} fontSize={fontSize} wrap="word" width={noteWidth} height={noteHeight} - ellipsis={true} /> {/* Use an invisible text block to work out text sizing */} From 68a5ae1f466771fe02bf27984f04b93bde2df036 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 7 Nov 2020 12:27:21 +1100 Subject: [PATCH 023/147] Remove int requirement from default token size --- src/components/token/TokenPreview.js | 2 +- src/components/token/TokenSettings.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/token/TokenPreview.js b/src/components/token/TokenPreview.js index 752a814..ef1d165 100644 --- a/src/components/token/TokenPreview.js +++ b/src/components/token/TokenPreview.js @@ -71,7 +71,7 @@ function TokenPreview({ token }) { const gridWidth = tokenWidth; const gridX = token.defaultSize; const gridSize = gridWidth / gridX; - const gridY = Math.ceil(tokenHeight / gridSize); + const gridY = Math.round(tokenHeight / gridSize); const gridHeight = gridY > 0 ? gridY * gridSize : tokenHeight; const borderWidth = Math.max( (Math.min(tokenWidth, gridHeight) / 200) * Math.max(1 / stageScale, 1), diff --git a/src/components/token/TokenSettings.js b/src/components/token/TokenSettings.js index a09310e..2d5147d 100644 --- a/src/components/token/TokenSettings.js +++ b/src/components/token/TokenSettings.js @@ -46,7 +46,7 @@ function TokenSettings({ token, onSettingsChange }) { name="tokenSize" value={`${(token && token.defaultSize) || 0}`} onChange={(e) => - onSettingsChange("defaultSize", parseInt(e.target.value)) + onSettingsChange("defaultSize", parseFloat(e.target.value)) } disabled={tokenEmpty || token.type === "default"} min={1} From e886da6dd902333bf57d35bcc91bf9193755f72c Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 7 Nov 2020 12:35:23 +1100 Subject: [PATCH 024/147] Remove dice share background colour --- src/components/party/DiceRolls.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/party/DiceRolls.js b/src/components/party/DiceRolls.js index 01d1ccc..bc05f1e 100644 --- a/src/components/party/DiceRolls.js +++ b/src/components/party/DiceRolls.js @@ -36,7 +36,6 @@ function DiceRolls({ rolls }) { {expanded && ( Date: Sat, 7 Nov 2020 12:40:20 +1100 Subject: [PATCH 025/147] Moved max map zoom to be based off of max grid size --- src/helpers/map.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/map.js b/src/helpers/map.js index a2101dc..8deeb70 100644 --- a/src/helpers/map.js +++ b/src/helpers/map.js @@ -161,7 +161,7 @@ export function getMapMaxZoom(map) { return 10; } // Return max grid size / 2 - return Math.max(Math.min(map.grid.size.x, map.grid.size.y) / 2, 5); + return Math.max(Math.max(map.grid.size.x, map.grid.size.y) / 2, 5); } export function snapNodeToMap( From 9eb70ac0a8cffa0f2aaae16899bf189d2d108578 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 12 Nov 2020 11:06:23 +1100 Subject: [PATCH 026/147] Removed unused note add icon --- src/icons/NoteAddIcon.js | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 src/icons/NoteAddIcon.js diff --git a/src/icons/NoteAddIcon.js b/src/icons/NoteAddIcon.js deleted file mode 100644 index 6ad5e28..0000000 --- a/src/icons/NoteAddIcon.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; - -function NoteAddIcon() { - return ( - - - - - - - - - ); -} - -export default NoteAddIcon; From dd41925b258173cdf1a23ca7108c539c923bf501 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 20 Nov 2020 09:08:13 +1100 Subject: [PATCH 027/147] Added decimal support to measure tool and added calculated precision --- src/components/map/MapMeasure.js | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js index bd96019..e27541c 100644 --- a/src/components/map/MapMeasure.js +++ b/src/components/map/MapMeasure.js @@ -21,11 +21,26 @@ function MapMeasure({ map, selectedToolSettings, active, gridSize }) { const [drawingShapeData, setDrawingShapeData] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); - const toolScale = - active && selectedToolSettings.scale.match(/(\d*)([a-zA-Z]*)/); - const toolMultiplier = - active && !isNaN(parseInt(toolScale[1])) ? parseInt(toolScale[1]) : 1; - const toolUnit = active && toolScale[2]; + function parseToolScale(scale) { + if (typeof scale === "string") { + const match = scale.match(/(\d*)(\.\d*)?([a-zA-Z]*)/); + const integer = parseFloat(match[1]); + const fractional = parseFloat(match[2]); + const unit = match[3] || ""; + if (!isNaN(integer) && !isNaN(fractional)) { + return { + multiplier: integer + fractional, + unit: unit, + digits: match[2].length - 1, + }; + } else if (!isNaN(integer) && isNaN(fractional)) { + return { multiplier: integer, unit: unit, digits: 0 }; + } + } + return { multiplier: 1, unit: "", digits: 0 }; + } + + const measureScale = parseToolScale(active && selectedToolSettings.scale); useEffect(() => { if (!active) { @@ -125,9 +140,9 @@ function MapMeasure({ map, selectedToolSettings, active, gridSize }) { > Date: Fri, 20 Nov 2020 12:00:59 +1100 Subject: [PATCH 028/147] Added alternating diagonals map measurement --- src/components/map/MapMeasure.js | 6 ++++-- .../map/controls/MeasureToolSettings.js | 9 +++++++++ src/helpers/vector2.js | 8 +++++++- src/icons/MeasureAlternatingIcon.js | 18 ++++++++++++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/icons/MeasureAlternatingIcon.js diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js index e27541c..54ef535 100644 --- a/src/components/map/MapMeasure.js +++ b/src/components/map/MapMeasure.js @@ -77,9 +77,11 @@ function MapMeasure({ map, selectedToolSettings, active, gridSize }) { brushPosition, gridSize ); + // Round the grid positions to the nearest 0.1 to aviod floating point issues + const precision = { x: 0.1, y: 0.1 }; const length = Vector2.distance( - Vector2.divide(points[0], gridSize), - Vector2.divide(points[1], gridSize), + Vector2.roundTo(Vector2.divide(points[0], gridSize), precision), + Vector2.roundTo(Vector2.divide(points[1], gridSize), precision), selectedToolSettings.type ); setDrawingShapeData({ diff --git a/src/components/map/controls/MeasureToolSettings.js b/src/components/map/controls/MeasureToolSettings.js index cb7a0e5..b163a01 100644 --- a/src/components/map/controls/MeasureToolSettings.js +++ b/src/components/map/controls/MeasureToolSettings.js @@ -5,6 +5,7 @@ import ToolSection from "./ToolSection"; import MeasureChebyshevIcon from "../../../icons/MeasureChebyshevIcon"; import MeasureEuclideanIcon from "../../../icons/MeasureEuclideanIcon"; import MeasureManhattanIcon from "../../../icons/MeasureManhattanIcon"; +import MeasureAlternatingIcon from "../../../icons/MeasureAlternatingIcon"; import Divider from "../../Divider"; @@ -19,6 +20,8 @@ function MeasureToolSettings({ settings, onSettingChange }) { onSettingChange({ type: "euclidean" }); } else if (key === "c") { onSettingChange({ type: "manhattan" }); + } else if (key === "a") { + onSettingChange({ type: "alternating" }); } } @@ -31,6 +34,12 @@ function MeasureToolSettings({ settings, onSettingChange }) { isSelected: settings.type === "chebyshev", icon: , }, + { + id: "alternating", + title: "Alternating Diagonal Distance (A)", + isSelected: settings.type === "alternating", + icon: , + }, { id: "euclidean", title: "Line Distance (L)", diff --git a/src/helpers/vector2.js b/src/helpers/vector2.js index 8eecb65..c2a6561 100644 --- a/src/helpers/vector2.js +++ b/src/helpers/vector2.js @@ -363,7 +363,7 @@ export function compare(a, b, threshold) { * Returns the distance between two vectors * @param {Vector2} a * @param {Vector2} b - * @param {string} type - `chebyshev | euclidean | manhattan` + * @param {string} type - `chebyshev | euclidean | manhattan | alternating` */ export function distance(a, b, type) { switch (type) { @@ -373,6 +373,12 @@ export function distance(a, b, type) { return length(subtract(a, b)); case "manhattan": return Math.abs(a.x - b.x) + Math.abs(a.y - b.y); + case "alternating": + // Alternating diagonal distance like D&D 3.5 and Pathfinder + const delta = abs(subtract(a, b)); + const ma = max(delta); + const mi = min(delta); + return ma - mi + Math.floor(1.5 * mi); default: return length(subtract(a, b)); } diff --git a/src/icons/MeasureAlternatingIcon.js b/src/icons/MeasureAlternatingIcon.js new file mode 100644 index 0000000..a65e504 --- /dev/null +++ b/src/icons/MeasureAlternatingIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function MeasureAlternatingIcon() { + return ( + + + + + ); +} + +export default MeasureAlternatingIcon; From b362d03bc6a52a2263840082c49c0d95c22b0459 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 20 Nov 2020 16:34:08 +1100 Subject: [PATCH 029/147] Updated socket.io --- package.json | 2 +- src/network/Session.js | 2 +- yarn.lock | 184 +++++++++++------------------------------ 3 files changed, 51 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index 9c7f5f8..29e085b 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "simple-peer": "feross/simple-peer#694/head", "simplebar-react": "^2.1.0", "simplify-js": "^1.2.4", - "socket.io-client": "^2.3.0", + "socket.io-client": "^3.0.3", "source-map-explorer": "^2.4.2", "theme-ui": "^0.3.1", "use-image": "^1.0.5", diff --git a/src/network/Session.js b/src/network/Session.js index d87f8dd..0333312 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -68,7 +68,7 @@ class Session extends EventEmitter { this.socket.on("signal", this._handleSignal.bind(this)); this.socket.on("auth error", this._handleAuthError.bind(this)); this.socket.on("disconnect", this._handleSocketDisconnect.bind(this)); - this.socket.on("reconnect", this._handleSocketReconnect.bind(this)); + this.socket.io.on("reconnect", this._handleSocketReconnect.bind(this)); this.peers = {}; diff --git a/yarn.lock b/yarn.lock index 3c6ecc0..e99fdda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2016,6 +2016,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/component-emitter@^1.2.10": + version "1.2.10" + resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.10.tgz#ef5b1589b9f16544642e473db5ea5639107ef3ea" + integrity sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg== + "@types/eslint@^7.2.4": version "7.2.5" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.2.5.tgz#92172ecf490c2fce4b076739693d75f30376d610" @@ -2546,11 +2551,6 @@ adjust-sourcemap-loader@3.0.0: loader-utils "^2.0.0" regex-parser "^2.2.11" -after@0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" - integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= - aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2755,11 +2755,6 @@ array.prototype.flatmap@^1.2.3: es-abstract "^1.18.0-next.1" function-bind "^1.1.1" -arraybuffer.slice@~0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" - integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== - arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" @@ -3095,10 +3090,10 @@ base64-arraybuffer-es6@0.6.0: resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.6.0.tgz#036f79f57588dca0018de7792ddf149299382007" integrity sha512-57nLqKj4ShsDwFJWJsM4sZx6u60WbCge35rWRSevUwqxDtRwwxiKAO800zD2upPv4CfdWjQp//wSLar35nDKvA== -base64-arraybuffer@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" - integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= +base64-arraybuffer@0.1.4: + version "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: version "1.5.1" @@ -3130,13 +3125,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -better-assert@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" - integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= - dependencies: - callsite "1.0.0" - bfj@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/bfj/-/bfj-7.0.2.tgz#1988ce76f3add9ac2913fd8ba47aad9e651bfbb2" @@ -3169,11 +3157,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -blob@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" - integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== - bluebird@^3.5.5: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -3482,11 +3465,6 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" -callsite@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" - integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= - callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" @@ -3855,21 +3833,11 @@ component-bind@1.0.0: resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= -component-emitter@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= - component-emitter@^1.2.1, component-emitter@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== -component-inherit@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" - integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= - compose-function@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-3.0.3.tgz#9ed675f13cc54501d30950a486ff6a7ba3ab185f" @@ -4421,13 +4389,6 @@ debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -4866,33 +4827,26 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" -engine.io-client@~3.4.0: - version "3.4.3" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c" - integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw== +engine.io-client@~4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-4.0.4.tgz#973d312ab9c8dc41e64d2c323982f3b04a7f749d" + integrity sha512-and4JRvjv+BQ4WBLopYUFePxju3ms3aBRk0XjaLdh/t9TKv2LCKtKKWFRoRzIfUZsu3U38FcYqNLuXhfS16vqw== dependencies: + base64-arraybuffer "0.1.4" component-emitter "~1.3.0" - component-inherit "0.0.3" debug "~4.1.0" - engine.io-parser "~2.2.0" + engine.io-parser "~4.0.1" has-cors "1.1.0" - indexof "0.0.1" - parseqs "0.0.5" - parseuri "0.0.5" - ws "~6.1.0" + parseqs "0.0.6" + parseuri "0.0.6" + ws "~7.2.1" xmlhttprequest-ssl "~1.5.4" yeast "0.1.2" -engine.io-parser@~2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" - integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== - dependencies: - after "0.8.2" - arraybuffer.slice "~0.0.7" - base64-arraybuffer "0.1.5" - blob "0.0.5" - has-binary2 "~1.0.2" +engine.io-parser@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.1.tgz#6444c3cf2523ba4fc3bbaedd4fe425e6bcb16479" + integrity sha512-v5aZK1hlckcJDGmHz3W8xvI3NUHYc9t8QtTbqdR5OaH3S9iJZilPubauOm+vLWOMMWzpE3hiq92l9lTAHamRCg== enhanced-resolve@^4.3.0: version "4.3.0" @@ -6026,13 +5980,6 @@ harmony-reflect@^1.4.6: resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.1.tgz#c108d4f2bb451efef7a37861fdbdae72c9bdefa9" integrity sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA== -has-binary2@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" - integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== - dependencies: - isarray "2.0.1" - has-cors@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" @@ -6445,11 +6392,6 @@ indexes-of@^1.0.1: resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc= -indexof@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" - integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= - infer-owner@^1.0.3, infer-owner@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" @@ -6914,11 +6856,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= -isarray@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" - integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -8439,11 +8376,6 @@ object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-component@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" - integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= - object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -8790,19 +8722,15 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== -parseqs@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" - integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= - dependencies: - better-assert "~1.0.0" +parseqs@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" + integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== -parseuri@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" - integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= - dependencies: - better-assert "~1.0.0" +parseuri@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" + integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" @@ -11107,34 +11035,27 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -socket.io-client@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" - integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== +socket.io-client@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-3.0.3.tgz#37890e24fc386e9de34358a1b7c4a2968f5d2630" + integrity sha512-kwCJAKb6JMqE9ZYXg78Dgt8rYLSwtJ/g/LJqpb/pOTFRZMSr1cKAsCaisHZ+IBwKHBY7DYOOkjtkHqseY3ZLpw== dependencies: + "@types/component-emitter" "^1.2.10" backo2 "1.0.2" - base64-arraybuffer "0.1.5" component-bind "1.0.0" - component-emitter "1.2.1" + component-emitter "~1.3.0" debug "~4.1.0" - engine.io-client "~3.4.0" - has-binary2 "~1.0.2" - has-cors "1.1.0" - indexof "0.0.1" - object-component "0.0.3" - parseqs "0.0.5" - parseuri "0.0.5" - socket.io-parser "~3.3.0" - to-array "0.1.4" + engine.io-client "~4.0.0" + parseuri "0.0.6" + socket.io-parser "~4.0.1" -socket.io-parser@~3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" - integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== +socket.io-parser@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.1.tgz#b8ec84364c7369ad32ae0c16dd4d388db19b3a04" + integrity sha512-5JfNykYptCwU2lkOI0ieoePWm+6stEhkZ2UnLDjqnE1YEjUlXXLd1lpxPZ+g+h3rtaytwWkWrLQCaJULlGqjOg== dependencies: - component-emitter "1.2.1" - debug "~3.1.0" - isarray "2.0.1" + component-emitter "~1.3.0" + debug "~4.1.0" sockjs-client@1.4.0: version "1.4.0" @@ -11836,11 +11757,6 @@ tmpl@1.0.x: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= -to-array@0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" - integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= - to-arraybuffer@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" @@ -12872,12 +12788,10 @@ ws@^7.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.0.tgz#a5dd76a24197940d4a8bb9e0e152bb4503764da7" integrity sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ== -ws@~6.1.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" - integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== - dependencies: - async-limiter "~1.0.0" +ws@~7.2.1: + version "7.2.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.5.tgz#abb1370d4626a5a9cd79d8de404aa18b3465d10d" + integrity sha512-C34cIU4+DB2vMyAbmEKossWq2ZQDr6QEyuuCzWrM9zfw1sGc0mYiJ0UnG9zzNykt49C2Fi34hvr2vssFQRS6EA== xml-name-validator@^3.0.0: version "3.0.0" From 179fd84293af65d50b50ca1737972d79c09d38e1 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 20 Nov 2020 16:34:14 +1100 Subject: [PATCH 030/147] Update .env.production --- .env.production | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.production b/.env.production index 9477201..e0aeb80 100644 --- a/.env.production +++ b/.env.production @@ -1,5 +1,5 @@ -REACT_APP_BROKER_URL=https://connect.owlbear.rodeo -REACT_APP_ICE_SERVERS_URL=https://connect.owlbear.rodeo/iceservers +REACT_APP_BROKER_URL=https://test.owlbear.rodeo +REACT_APP_ICE_SERVERS_URL=https://test.owlbear.rodeo/iceservers REACT_APP_STRIPE_API_KEY=pk_live_MJjzi5djj524Y7h3fL5PNh4e00a852XD51 REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo REACT_APP_VERSION=$npm_package_version \ No newline at end of file From f028a011ca16c620a547a7af8c4275ebfc3c929d Mon Sep 17 00:00:00 2001 From: Nicola Date: Sat, 21 Nov 2020 15:46:36 +1100 Subject: [PATCH 031/147] Changed transports to be websocket only --- src/network/Session.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/network/Session.js b/src/network/Session.js index 0333312..f1a02d4 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -57,7 +57,9 @@ class Session extends EventEmitter { constructor() { super(); - this.socket = io(process.env.REACT_APP_BROKER_URL); + this.socket = io(process.env.REACT_APP_BROKER_URL, { + transports: ["websocket"] + }); this.socket.on( "party member joined", From 4fee6d278a15348c155dd723bac64fb2ae08f152 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 21 Nov 2020 16:01:37 +1100 Subject: [PATCH 032/147] Moved handle party joined to array --- src/network/Session.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/network/Session.js b/src/network/Session.js index f1a02d4..0ffb7a3 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -221,9 +221,10 @@ class Session extends EventEmitter { } _handleJoinedParty(otherIds) { - for (let [index, id] of otherIds.entries()) { + for (let i = 0; i < otherIds.length; i++) { + const id = otherIds[i]; // Send a sync request to the first member of the party - const sync = index === 0; + const sync = i === 0; this._addPeer(id, true, sync); } this.emit("authenticationSuccess"); From 61d6a7bb8558302a01a36e1fd58c69f927dffb98 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 21 Nov 2020 16:08:26 +1100 Subject: [PATCH 033/147] Disabled sentry --- src/helpers/logging.js | 6 +++--- src/index.js | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/helpers/logging.js b/src/helpers/logging.js index a80ff82..4b06f97 100644 --- a/src/helpers/logging.js +++ b/src/helpers/logging.js @@ -2,7 +2,7 @@ import { captureException } from "@sentry/react"; export function logError(error) { console.error(error); - if (process.env.NODE_ENV === "production") { - captureException(error); - } + // if (process.env.NODE_ENV === "production") { + // captureException(error); + // } } diff --git a/src/index.js b/src/index.js index 65de3d1..7001e2a 100644 --- a/src/index.js +++ b/src/index.js @@ -11,15 +11,15 @@ import * as serviceWorker from "./serviceWorker"; import "./index.css"; -if (process.env.NODE_ENV === "production") { - Sentry.init({ - dsn: - "https://bc1e2edfe7ca453f8e7357a48693979e@o467475.ingest.sentry.io/5493956", - release: "owlbear-rodeo@" + process.env.REACT_APP_VERSION, - // Ignore resize error as it is triggered by going fullscreen on slower computers - ignoreErrors: ["ResizeObserver loop limit exceeded"], - }); -} +// if (process.env.NODE_ENV === "production") { +// Sentry.init({ +// dsn: +// "https://bc1e2edfe7ca453f8e7357a48693979e@o467475.ingest.sentry.io/5493956", +// release: "owlbear-rodeo@" + process.env.REACT_APP_VERSION, +// // Ignore resize error as it is triggered by going fullscreen on slower computers +// ignoreErrors: ["ResizeObserver loop limit exceeded"], +// }); +// } Modal.setAppElement("#root"); From 575b566b5339c1825ee57fb33bd1a8e0bdedc2cc Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 21 Nov 2020 16:15:04 +1100 Subject: [PATCH 034/147] Removed JSON parse from websocket messages --- src/network/Session.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/Session.js b/src/network/Session.js index 0ffb7a3..0b62980 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -58,7 +58,7 @@ class Session extends EventEmitter { constructor() { super(); this.socket = io(process.env.REACT_APP_BROKER_URL, { - transports: ["websocket"] + transports: ["websocket"], }); this.socket.on( @@ -232,7 +232,7 @@ class Session extends EventEmitter { } _handleSignal(data) { - const { from, signal } = JSON.parse(data); + const { from, signal } = data; if (from in this.peers) { this.peers[from].connection.signal(signal); } From 9c91507eb306b45db1dbcce756e1c431ee978d0a Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Sun, 22 Nov 2020 10:42:46 +1100 Subject: [PATCH 035/147] Added polling transport to socket io --- src/network/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/Session.js b/src/network/Session.js index 0b62980..03f0c07 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -58,7 +58,7 @@ class Session extends EventEmitter { constructor() { super(); this.socket = io(process.env.REACT_APP_BROKER_URL, { - transports: ["websocket"], + transports: ["polling", "websocket"], }); this.socket.on( From e98116c29044a43d1142184cf9020dfd636c42d0 Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Sun, 22 Nov 2020 10:47:28 +1100 Subject: [PATCH 036/147] Revert "Added polling transport to socket io" This reverts commit 9c91507eb306b45db1dbcce756e1c431ee978d0a. --- src/network/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/Session.js b/src/network/Session.js index 03f0c07..0b62980 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -58,7 +58,7 @@ class Session extends EventEmitter { constructor() { super(); this.socket = io(process.env.REACT_APP_BROKER_URL, { - transports: ["polling", "websocket"], + transports: ["websocket"], }); this.socket.on( From fdb5d8c458db69d3ecbb9367b18b2a5d2a8f61bd Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Sun, 22 Nov 2020 20:19:32 +1100 Subject: [PATCH 037/147] Add extra header options to socket io request --- src/network/Session.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/network/Session.js b/src/network/Session.js index 0b62980..da532c8 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -58,7 +58,14 @@ class Session extends EventEmitter { constructor() { super(); this.socket = io(process.env.REACT_APP_BROKER_URL, { - transports: ["websocket"], + transports: ["polling", "websocket"], + transportOptions: { + polling: { + extraHeaders: { + "Cookie":"session-persist" + } + } + } }); this.socket.on( From 8f3ebb83164c8f4b369dbba32588d432012df6af Mon Sep 17 00:00:00 2001 From: Nicola Thouliss Date: Sun, 22 Nov 2020 20:38:11 +1100 Subject: [PATCH 038/147] Revert "Add extra header options to socket io request" This reverts commit fdb5d8c458db69d3ecbb9367b18b2a5d2a8f61bd. --- src/network/Session.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/network/Session.js b/src/network/Session.js index da532c8..0b62980 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -58,14 +58,7 @@ class Session extends EventEmitter { constructor() { super(); this.socket = io(process.env.REACT_APP_BROKER_URL, { - transports: ["polling", "websocket"], - transportOptions: { - polling: { - extraHeaders: { - "Cookie":"session-persist" - } - } - } + transports: ["websocket"], }); this.socket.on( From 5209317d1e2cfdfc59e336b050719acc14c1d842 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 11:09:48 +1100 Subject: [PATCH 039/147] Added offline mode --- src/network/Session.js | 72 +++++++++++++++++++++++++----------------- src/routes/Game.js | 33 ++++++++++++++----- 2 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/network/Session.js b/src/network/Session.js index 0b62980..85adc4c 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -27,8 +27,8 @@ import { logError } from "../helpers/logging"; * - error * - authenticationSuccess * - authenticationError - * - connected: You have connected - * - disconnected: You have disconnected + * - connected: You have connected to the party + * - disconnected: You have disconnected from the party */ class Session extends EventEmitter { /** @@ -45,8 +45,15 @@ class Session extends EventEmitter { */ peers; + /** + * The state of the session + * + * @type {('unknown'|'online'|'offline')} + */ + state; + get id() { - return this.socket.id; + return this.socket && this.socket.id; } _iceServers; @@ -57,27 +64,43 @@ class Session extends EventEmitter { constructor() { super(); - this.socket = io(process.env.REACT_APP_BROKER_URL, { - transports: ["websocket"], - }); - - this.socket.on( - "party member joined", - this._handlePartyMemberJoined.bind(this) - ); - this.socket.on("party member left", this._handlePartyMemberLeft.bind(this)); - this.socket.on("joined party", this._handleJoinedParty.bind(this)); - this.socket.on("signal", this._handleSignal.bind(this)); - this.socket.on("auth error", this._handleAuthError.bind(this)); - this.socket.on("disconnect", this._handleSocketDisconnect.bind(this)); - this.socket.io.on("reconnect", this._handleSocketReconnect.bind(this)); - this.peers = {}; - + this.state = "unknown"; // Signal connected peers of a closure on refresh window.addEventListener("beforeunload", this._handleUnload.bind(this)); } + async connect() { + try { + const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL); + const data = await response.json(); + this._iceServers = data.iceServers; + + this.socket = io(process.env.REACT_APP_BROKER_URL, { + transports: ["websocket"], + }); + + this.socket.on( + "party member joined", + this._handlePartyMemberJoined.bind(this) + ); + this.socket.on( + "party member left", + this._handlePartyMemberLeft.bind(this) + ); + this.socket.on("joined party", this._handleJoinedParty.bind(this)); + this.socket.on("signal", this._handleSignal.bind(this)); + this.socket.on("auth error", this._handleAuthError.bind(this)); + this.socket.on("disconnect", this._handleSocketDisconnect.bind(this)); + this.socket.io.on("reconnect", this._handleSocketReconnect.bind(this)); + + this.state = "online"; + } catch (error) { + logError(error); + this.state = "offline"; + } + } + /** * Send data to all connected peers * @@ -104,21 +127,12 @@ class Session extends EventEmitter { partyId, password ); - this.emit("disconnected"); return; } this._partyId = partyId; this._password = password; - try { - const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL); - const data = await response.json(); - this._iceServers = data.iceServers; - this.socket.emit("join party", partyId, password); - } catch (error) { - logError(error); - this.emit("disconnected"); - } + this.socket.emit("join party", partyId, password); } _addPeer(id, initiator, sync) { diff --git a/src/routes/Game.js b/src/routes/Game.js index 83953a6..da945ea 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.js @@ -17,8 +17,6 @@ import NetworkedParty from "../network/NetworkedParty"; import Session from "../network/Session"; -const session = new Session(); - function Game() { const { id: gameId } = useParams(); const { @@ -28,6 +26,18 @@ function Game() { } = useContext(AuthContext); const { databaseStatus } = useContext(DatabaseContext); + const [session] = useState(new Session()); + const [offline, setOffline] = useState(false); + useEffect(() => { + async function connect() { + await session.connect(); + if (session.state === "offline") { + setOffline(true); + } + } + connect(); + }, [session]); + // Handle authentication status useEffect(() => { function handleAuthSuccess() { @@ -43,7 +53,7 @@ function Game() { session.off("authenticationSuccess", handleAuthSuccess); session.off("authenticationError", handleAuthError); }; - }, [setAuthenticationStatus]); + }, [setAuthenticationStatus, session]); // Handle session errors const [peerError, setPeerError] = useState(null); @@ -59,7 +69,7 @@ function Game() { return () => { session.off("error", handlePeerError); }; - }, []); + }, [session]); // Handle connection const [connected, setConnected] = useState(false); @@ -79,14 +89,14 @@ function Game() { session.off("connected", handleConnected); session.off("disconnected", handleDisconnected); }; - }, []); + }, [session]); // Join game useEffect(() => { - if (databaseStatus !== "loading") { + if (session.state === "online" && databaseStatus !== "loading") { session.joinParty(gameId, password); } - }, [gameId, password, databaseStatus]); + }, [gameId, password, databaseStatus, session]); // A ref to the Konva stage // the ref will be assigned in the MapInteraction component @@ -114,6 +124,13 @@ function Game() { + {}} allowClose={false}> + + + Offline, refresh to reconnect. + + + {}} @@ -126,7 +143,7 @@ function Game() { - {authenticationStatus === "unknown" && } + {authenticationStatus === "unknown" && !offline && } ); } From 794ba22689351ccb8e13fd985332bd3c2f5fdafd Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 13:17:32 +1100 Subject: [PATCH 040/147] Reworded offline message --- src/routes/Game.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Game.js b/src/routes/Game.js index da945ea..84d75f4 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.js @@ -127,7 +127,7 @@ function Game() { {}} allowClose={false}> - Offline, refresh to reconnect. + Unable to connect to game, refresh to reconnect. From 9ee3efbadbf9a60ac652ebb32aaaf2f9a30c01e8 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 13:17:53 +1100 Subject: [PATCH 041/147] Added storage usage summary to settings modal --- package.json | 1 + src/modals/SettingsModal.js | 34 ++++++++++++++++++++++++++++++++-- yarn.lock | 2 +- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 29e085b..12a68ac 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lodash.set": "^4.3.2", "normalize-wheel": "^1.0.1", "polygon-clipping": "^0.15.1", + "pretty-bytes": "^5.4.1", "raw.macro": "^0.4.2", "react": "^17.0.1", "react-dom": "^17.0.1", diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js index 126d371..862d289 100644 --- a/src/modals/SettingsModal.js +++ b/src/modals/SettingsModal.js @@ -1,5 +1,14 @@ -import React, { useState, useContext } from "react"; -import { Label, Flex, Button, useColorMode, Checkbox, Divider } from "theme-ui"; +import React, { useState, useContext, useEffect } from "react"; +import { + Label, + Flex, + Button, + useColorMode, + Checkbox, + Divider, + Text, +} from "theme-ui"; +import prettyBytes from "pretty-bytes"; import Modal from "../components/Modal"; import Slider from "../components/Slider"; @@ -16,6 +25,16 @@ function SettingsModal({ isOpen, onRequestClose }) { const { userId } = useContext(AuthContext); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [labelSize, setLabelSize] = useSetting("map.labelSize"); + const [storageEstimate, setStorageEstimate] = useState({ + usage: 0, + quota: 0, + }); + + useEffect(() => { + if (isOpen) { + navigator.storage.estimate().then(setStorageEstimate); + } + }, [isOpen]); async function handleEraseAllData() { localStorage.clear(); @@ -96,6 +115,17 @@ function SettingsModal({ isOpen, onRequestClose }) { Erase all content and reset + + + Storage Used: {prettyBytes(storageEstimate.usage)} of{" "} + {prettyBytes(storageEstimate.quota)} ( + {Math.round( + (storageEstimate.usage / Math.max(storageEstimate.quota, 1)) * + 100 + )} + %) + + Date: Thu, 26 Nov 2020 15:24:33 +1100 Subject: [PATCH 042/147] Added banner for quota exceeded error --- src/contexts/DatabaseContext.js | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/contexts/DatabaseContext.js b/src/contexts/DatabaseContext.js index 98ee4d0..323b49a 100644 --- a/src/contexts/DatabaseContext.js +++ b/src/contexts/DatabaseContext.js @@ -1,4 +1,7 @@ import React, { useState, useEffect } from "react"; +import { Box, Text } from "theme-ui"; + +import Banner from "../components/Banner"; import { getDatabase } from "../database"; @@ -7,6 +10,7 @@ const DatabaseContext = React.createContext(); export function DatabaseProvider({ children }) { const [database, setDatabase] = useState(); const [databaseStatus, setDatabaseStatus] = useState("loading"); + const [databaseError, setDatabaseError] = useState(); useEffect(() => { // Create a test database and open it to see if indexedDB is enabled @@ -34,15 +38,43 @@ export function DatabaseProvider({ children }) { await db.open(); window.indexedDB.deleteDatabase("__test"); }; + + function handleDatabaseError(event) { + if (event.reason.name === "QuotaExceededError") { + event.preventDefault(); + setDatabaseError({ + name: event.reason.name, + message: "Storage Quota Exceeded Please Clear Space and Try Again.", + }); + } + } + window.addEventListener("unhandledrejection", handleDatabaseError); + + return () => { + window.removeEventListener("unhandledrejection", handleDatabaseError); + }; }, []); const value = { database, databaseStatus, + databaseError, }; return ( - {children} + <> + {children} + setDatabaseError()} + > + + + {databaseError && databaseError.message} + + + + ); } From 8160a98c495ca302da1870412d1dbe38c00aa40f Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 16:27:41 +1100 Subject: [PATCH 043/147] Add quota error to sentry ignore --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 7001e2a..1f42ef5 100644 --- a/src/index.js +++ b/src/index.js @@ -17,7 +17,7 @@ import "./index.css"; // "https://bc1e2edfe7ca453f8e7357a48693979e@o467475.ingest.sentry.io/5493956", // release: "owlbear-rodeo@" + process.env.REACT_APP_VERSION, // // Ignore resize error as it is triggered by going fullscreen on slower computers -// ignoreErrors: ["ResizeObserver loop limit exceeded"], +// ignoreErrors: ["ResizeObserver loop limit exceeded", "AbortError: QuotaExceededError"], // }); // } From aaa793ad7137f010221fd2f73871faf63a6f7907 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 16:29:10 +1100 Subject: [PATCH 044/147] Added multi-threaded initial map and token loading --- package.json | 4 ++++ src/contexts/MapDataContext.js | 15 ++++++++++++--- src/contexts/TokenDataContext.js | 12 +++++++++--- src/modals/SelectMapModal.js | 3 ++- src/modals/SelectTokensModal.js | 12 +++++++++--- src/workers/DatabaseWorker.js | 16 ++++++++++++++++ yarn.lock | 13 +++++++++++++ 7 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 src/workers/DatabaseWorker.js diff --git a/package.json b/package.json index 12a68ac..b038cdb 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@testing-library/user-event": "^12.2.2", "ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778", "case": "^1.6.3", + "comlink": "^4.3.0", "dexie": "^3.0.3", "err-code": "^2.0.3", "fake-indexeddb": "^3.1.2", @@ -71,5 +72,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "worker-loader": "^3.0.5" } } diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index 9d7e4b4..2119e23 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -1,8 +1,11 @@ import React, { useEffect, useState, useContext } from "react"; +import * as Comlink from "comlink"; import AuthContext from "./AuthContext"; import DatabaseContext from "./DatabaseContext"; +import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax + import { maps as defaultMaps } from "../maps"; const MapDataContext = React.createContext(); @@ -29,6 +32,8 @@ export function MapDataProvider({ children }) { const [maps, setMaps] = useState([]); const [mapStates, setMapStates] = useState([]); + const [mapsLoading, setMapsLoading] = useState(true); + // Load maps from the database and ensure state is properly setup useEffect(() => { if (!userId || !database || databaseStatus === "loading") { @@ -60,15 +65,16 @@ export function MapDataProvider({ children }) { } async function loadMaps() { - let storedMaps = []; - // Use a cursor instead of toArray to prevent IPC max size error - await database.table("maps").each((map) => storedMaps.push(map)); + const worker = Comlink.wrap(new DatabaseWorker()); + await worker.loadData("maps"); + const storedMaps = await worker.data; const sortedMaps = storedMaps.sort((a, b) => b.created - a.created); const defaultMapsWithIds = await getDefaultMaps(); const allMaps = [...sortedMaps, ...defaultMapsWithIds]; setMaps(allMaps); const storedStates = await database.table("states").toArray(); setMapStates(storedStates); + setMapsLoading(false); } loadMaps(); @@ -137,8 +143,10 @@ export function MapDataProvider({ children }) { try { await database.table("maps").update(id, update); } catch (error) { + // if (error.name !== "QuotaExceededError") { const map = (await getMapFromDB(id)) || {}; await database.table("maps").put({ ...map, id, ...update }); + // } } setMaps((prevMaps) => { const newMaps = [...prevMaps]; @@ -247,6 +255,7 @@ export function MapDataProvider({ children }) { putMap, getMap, getMapFromDB, + mapsLoading, }; return ( {children} diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index ed576de..f94026d 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -1,8 +1,11 @@ import React, { useEffect, useState, useContext } from "react"; +import * as Comlink from "comlink"; import AuthContext from "./AuthContext"; import DatabaseContext from "./DatabaseContext"; +import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax + import { tokens as defaultTokens } from "../tokens"; const TokenDataContext = React.createContext(); @@ -14,6 +17,7 @@ export function TokenDataProvider({ children }) { const { userId } = useContext(AuthContext); const [tokens, setTokens] = useState([]); + const [tokensLoading, setTokensLoading] = useState(true); useEffect(() => { if (!userId || !database || databaseStatus === "loading") { @@ -33,13 +37,14 @@ export function TokenDataProvider({ children }) { } async function loadTokens() { - let storedTokens = []; - // Use a cursor instead of toArray to prevent IPC max size error - await database.table("tokens").each((token) => storedTokens.push(token)); + const worker = Comlink.wrap(new DatabaseWorker()); + await worker.loadData("tokens"); + const storedTokens = await worker.data; const sortedTokens = storedTokens.sort((a, b) => b.created - a.created); const defaultTokensWithIds = getDefaultTokes(); const allTokens = [...sortedTokens, ...defaultTokensWithIds]; setTokens(allTokens); + setTokensLoading(false); } loadTokens(); @@ -160,6 +165,7 @@ export function TokenDataProvider({ children }) { putToken, getToken, tokensById, + tokensLoading, }; return ( diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 8f745f1..96e7184 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -53,6 +53,7 @@ function SelectMapModal({ resetMap, updateMap, updateMaps, + mapsLoading, } = useContext(MapDataContext); /** @@ -388,7 +389,7 @@ function SelectMapModal({ - {imageLoading && } + {(imageLoading || mapsLoading) && } setIsEditModalOpen(false)} diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index 8351654..f13c3e5 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -10,6 +10,7 @@ import ConfirmModal from "./ConfirmModal"; import Modal from "../components/Modal"; import ImageDrop from "../components/ImageDrop"; import TokenTiles from "../components/token/TokenTiles"; +import LoadingOverlay from "../components/LoadingOverlay"; import blobToBuffer from "../helpers/blobToBuffer"; import useKeyboard from "../helpers/useKeyboard"; @@ -20,9 +21,13 @@ import AuthContext from "../contexts/AuthContext"; function SelectTokensModal({ isOpen, onRequestClose }) { const { userId } = useContext(AuthContext); - const { ownedTokens, addToken, removeTokens, updateTokens } = useContext( - TokenDataContext - ); + const { + ownedTokens, + addToken, + removeTokens, + updateTokens, + tokensLoading, + } = useContext(TokenDataContext); /** * Search @@ -256,6 +261,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) { + {tokensLoading && } setIsEditModalOpen(false)} diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js new file mode 100644 index 0000000..2987547 --- /dev/null +++ b/src/workers/DatabaseWorker.js @@ -0,0 +1,16 @@ +import * as Comlink from "comlink"; + +import { getDatabase } from "../database"; + +// Worker to load large amounts of database data on a separate thread +let obj = { + data: [], + async loadData(table) { + let db = getDatabase({}); + this.data = []; + // Use a cursor instead of toArray to prevent IPC max size error + await db.table(table).each((map) => this.data.push(map)); + }, +}; + +Comlink.expose(obj); diff --git a/yarn.lock b/yarn.lock index a9321ba..ffd0ea8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3808,6 +3808,11 @@ combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comlink@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.3.0.tgz#80b3366baccd87897dab3638ebfcfae28b2f87c7" + integrity sha512-mu4KKKNuW8TvkfpW/H88HBPeILubBS6T94BdD1VWBXNXfiyqVtwUCVNO1GeNOBTsIswzsMjWlycYr+77F5b84g== + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -12720,6 +12725,14 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" +worker-loader@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-3.0.5.tgz#6e13a583c4120ba419eece8e4f2e098b014311bf" + integrity sha512-cOh4UqTtvT8eHpyuuTK2C66Fg/G5Pb7g11bwtKm7uyD0vj2hCGY1APlSzVD75V9ciYZt44VPbFPiSFTSLxkQ+w== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + worker-rpc@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" From 7ac555baa0adec1809a8868dd1005601b877c124 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 16:32:48 +1100 Subject: [PATCH 045/147] Fix null reference error when image upload dialog is closed when image upload finishes --- src/modals/SelectMapModal.js | 4 +++- src/modals/SelectTokensModal.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 96e7184..d9930f6 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -95,7 +95,9 @@ function SelectMapModal({ await handleImageUpload(file); } // Set file input to null to allow adding the same image 2 times in a row - fileInputRef.current.value = null; + if (fileInputRef.current) { + fileInputRef.current.value = null; + } } async function handleImageUpload(file) { diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index f13c3e5..89ee6ee 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -74,7 +74,9 @@ function SelectTokensModal({ isOpen, onRequestClose }) { await handleImageUpload(file); } // Set file input to null to allow adding the same image 2 times in a row - fileInputRef.current.value = null; + if (fileInputRef.current) { + fileInputRef.current.value = null; + } } async function handleImageUpload(file) { From 025304ee44d6e5204c13701c8a33559e38f0819d Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 17:08:09 +1100 Subject: [PATCH 046/147] Fix database worker for when indexeddb is disabled --- src/workers/DatabaseWorker.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index 2987547..4c5fff3 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -6,10 +6,12 @@ import { getDatabase } from "../database"; let obj = { data: [], async loadData(table) { - let db = getDatabase({}); this.data = []; - // Use a cursor instead of toArray to prevent IPC max size error - await db.table(table).each((map) => this.data.push(map)); + try { + let db = getDatabase({}); + // Use a cursor instead of toArray to prevent IPC max size error + await db.table(table).each((map) => this.data.push(map)); + } catch {} }, }; From d231d9340ea5db0bf0ae9e087b89b9a4a83335d5 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 17:08:25 +1100 Subject: [PATCH 047/147] Fix styling of indexeddb disabled warning --- src/components/map/MapTiles.js | 2 ++ src/components/token/TokenTiles.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js index e88a75d..3e7f3f1 100644 --- a/src/components/map/MapTiles.js +++ b/src/components/map/MapTiles.js @@ -87,6 +87,7 @@ function MapTiles({ Date: Thu, 26 Nov 2020 18:54:11 +1100 Subject: [PATCH 048/147] Fixed party joining --- src/routes/Game.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/routes/Game.js b/src/routes/Game.js index 84d75f4..93535d9 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.js @@ -27,13 +27,11 @@ function Game() { const { databaseStatus } = useContext(DatabaseContext); const [session] = useState(new Session()); - const [offline, setOffline] = useState(false); + const [offline, setOffline] = useState(); useEffect(() => { async function connect() { await session.connect(); - if (session.state === "offline") { - setOffline(true); - } + setOffline(session.state === "offline"); } connect(); }, [session]); @@ -96,7 +94,7 @@ function Game() { if (session.state === "online" && databaseStatus !== "loading") { session.joinParty(gameId, password); } - }, [gameId, password, databaseStatus, session]); + }, [gameId, password, databaseStatus, session, offline]); // A ref to the Konva stage // the ref will be assigned in the MapInteraction component From 11116e86861120bc0b905cf74544752dd5a830c7 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 18:54:32 +1100 Subject: [PATCH 049/147] Throw error when ice servers responds not ok --- src/network/Session.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/network/Session.js b/src/network/Session.js index 85adc4c..ed5004f 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -73,6 +73,9 @@ class Session extends EventEmitter { async connect() { try { const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL); + if (!response.ok) { + throw Error(); + } const data = await response.json(); this._iceServers = data.iceServers; From 700434a50c579cf23db5b3bd31f523e2a298cbbc Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 26 Nov 2020 18:55:27 +1100 Subject: [PATCH 050/147] Added error message to ice servers fetch fail --- src/network/Session.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/network/Session.js b/src/network/Session.js index ed5004f..2c10bf7 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -74,7 +74,7 @@ class Session extends EventEmitter { try { const response = await fetch(process.env.REACT_APP_ICE_SERVERS_URL); if (!response.ok) { - throw Error(); + throw Error("Unable to fetch ICE servers"); } const data = await response.json(); this._iceServers = data.iceServers; From fca1c9fd1f48fc6b93d2c9ffd1bb3ff5ccb7a24d Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 27 Nov 2020 11:19:52 +1100 Subject: [PATCH 051/147] Added margin to markdown list and added default props --- src/components/Markdown.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Markdown.js b/src/components/Markdown.js index b49e564..a9381c5 100644 --- a/src/components/Markdown.js +++ b/src/components/Markdown.js @@ -45,7 +45,7 @@ function Image(props) { } function ListItem(props) { - return ; + return ; } function Code({ children, value }) { @@ -157,4 +157,8 @@ function Markdown({ source, assets }) { ); } +Markdown.defaultProps = { + assets: {}, +}; + export default Markdown; From ad9428c703ad5fcb9b7de7900b7c51ad0c34f4ad Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 27 Nov 2020 11:22:58 +1100 Subject: [PATCH 052/147] Increased line height for body 2 font --- src/theme.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/theme.js b/src/theme.js index 6a1d4c8..35dba32 100644 --- a/src/theme.js +++ b/src/theme.js @@ -40,6 +40,7 @@ const theme = { }, lineHeights: { body: 1.3, + body2: 1.5, display: 1.1, heading: 1.25, }, @@ -68,6 +69,7 @@ const theme = { fontFamily: "body2", fontSize: 0, fontWeight: "body", + lineHeight: "body2", }, }, styles: { From 3cb1014ed26bd6f11ae8baf3bcd90f00a3fe0514 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 27 Nov 2020 11:23:20 +1100 Subject: [PATCH 053/147] Added getting started modal to home screen --- src/docs/howTo/gettingStarted.md | 11 +++++++++++ src/icons/HelpIcon.js | 19 +++++++++++++++++++ src/modals/GettingStartedModal.js | 29 +++++++++++++++++++++++++++++ src/routes/Home.js | 23 +++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 src/docs/howTo/gettingStarted.md create mode 100644 src/icons/HelpIcon.js create mode 100644 src/modals/GettingStartedModal.js diff --git a/src/docs/howTo/gettingStarted.md b/src/docs/howTo/gettingStarted.md new file mode 100644 index 0000000..2736788 --- /dev/null +++ b/src/docs/howTo/gettingStarted.md @@ -0,0 +1,11 @@ +1. Start a game to generate a unique URL that can connect you and + your players. + + Each game is recycled after 24 hours so make sure you create a new game when you play your next session. + +2. Invite players with your unique URL from step 1. +3. Share a map, roll dice or share audio with your players. + + All data is saved automatically to your computer so next session simply use the same browser and all your maps and tokens will be ready to go. + +That's it, no accounts, no paywalls, no ads, just a virtual tabletop. diff --git a/src/icons/HelpIcon.js b/src/icons/HelpIcon.js new file mode 100644 index 0000000..f45c9a8 --- /dev/null +++ b/src/icons/HelpIcon.js @@ -0,0 +1,19 @@ +import React from "react"; + +function HelpIcon() { + return ( + + + + + ); +} + +export default HelpIcon; diff --git a/src/modals/GettingStartedModal.js b/src/modals/GettingStartedModal.js new file mode 100644 index 0000000..9a8e157 --- /dev/null +++ b/src/modals/GettingStartedModal.js @@ -0,0 +1,29 @@ +import React from "react"; +import { Box, Label, Text } from "theme-ui"; +import raw from "raw.macro"; + +import Modal from "../components/Modal"; +import Markdown from "../components/Markdown"; +import Link from "../components/Link"; + +const gettingStarted = raw("../docs/howTo/gettingStarted.md"); + +function GettingStartedModal({ isOpen, onRequestClose }) { + return ( + + + + + + For more tutorials visit the How To page + + + + ); +} + +export default GettingStartedModal; diff --git a/src/routes/Home.js b/src/routes/Home.js index 76e5952..072c04d 100644 --- a/src/routes/Home.js +++ b/src/routes/Home.js @@ -6,6 +6,9 @@ import Footer from "../components/Footer"; import StartModal from "../modals/StartModal"; import JoinModal from "../modals/JoinModal"; +import GettingStartedModal from "../modals/GettingStartedModal"; + +import HelpIcon from "../icons/HelpIcon"; import AuthContext from "../contexts/AuthContext"; @@ -19,6 +22,9 @@ import owlington from "../images/Owlington.png"; function Home() { const [isStartModalOpen, setIsStartModalOpen] = useState(false); const [isJoinModalOpen, setIsJoinModalOpen] = useState(false); + const [isGettingStartedModalOpen, setIsGettingStartedModalOpen] = useState( + false + ); // Reset password on visiting home const { setPassword } = useContext(AuthContext); @@ -50,6 +56,18 @@ function Home() { Owlbear Rodeo + @@ -76,6 +94,7 @@ function Home() { > Donate + @@ -101,6 +120,10 @@ function Home() { isOpen={isStartModalOpen} onRequestClose={() => setIsStartModalOpen(false)} /> + setIsGettingStartedModalOpen(false)} + />