diff --git a/src/components/map/Map.js b/src/components/map/Map.js index ae09b02..a6ce762 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -228,7 +228,9 @@ function Map({ }) } draggable={ - selectedToolId === "pan" && !(tokenState.id in disabledTokens) + selectedToolId === "pan" && + !(tokenState.id in disabledTokens) && + !tokenState.locked } mapState={mapState} fadeOnHover={selectedToolId === "drawing"} @@ -245,6 +247,7 @@ function Map({ onTokenStateChange={onMapTokenStateChange} tokenState={mapState && mapState.tokens[tokenMenuOptions.tokenStateId]} tokenImage={tokenMenuOptions.tokenImage} + map={map} /> ); diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index ad9cd8d..0622457 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -113,7 +113,8 @@ function MapToken({ ...mapState.tokens[mountedToken.id()], x: mountedToken.x() / mapWidth, y: mountedToken.y() / mapHeight, - lastEditedBy: userId, + lastModifiedBy: userId, + lastModified: Date.now(), }; } } @@ -125,7 +126,8 @@ function MapToken({ ...tokenState, x: tokenGroup.x() / mapWidth, y: tokenGroup.y() / mapHeight, - lastEditedBy: userId, + lastModifiedBy: userId, + lastModified: Date.now(), }, }); onTokenDragEnd(event); @@ -139,16 +141,37 @@ function MapToken({ } const [tokenOpacity, setTokenOpacity] = useState(1); - function handlePointerDown() { + // Store token pointer down position to check for a click when token is locked + const tokenPointerDownPositionRef = useRef(); + function handlePointerDown(event) { if (draggable) { setPreventMapInteraction(true); } + if (tokenState.locked && map.owner === userId) { + const pointerPosition = { x: event.evt.clientX, y: event.evt.clientY }; + tokenPointerDownPositionRef.current = pointerPosition; + } } - function handlePointerUp() { + function handlePointerUp(event) { if (draggable) { setPreventMapInteraction(false); } + // Check token click when locked and we are the map owner + // We can't use onClick because that doesn't check pointer distance + if (tokenState.locked && map.owner === userId) { + // If down and up distance is small trigger a click + const pointerPosition = { x: event.evt.clientX, y: event.evt.clientY }; + const distance = Vector2.distance( + tokenPointerDownPositionRef.current, + pointerPosition, + "euclidean" + ); + if (distance < 5) { + const tokenImage = event.target; + onTokenMenuOpen(tokenState.id, tokenImage); + } + } } function handlePointerEnter() { @@ -192,13 +215,18 @@ function MapToken({ const previousWidth = usePrevious(mapWidth); const previousHeight = usePrevious(mapHeight); const resized = mapWidth !== previousWidth || mapHeight !== previousHeight; - const skipAnimation = tokenState.lastEditedBy === userId || resized; + const skipAnimation = tokenState.lastModifiedBy === userId || resized; const props = useSpring({ x: tokenX, y: tokenY, immediate: skipAnimation, }); + // When a token is hidden if you aren't the map owner hide it completely + if (map && !tokenState.visible && map.owner !== userId) { + return null; + } + return ( diff --git a/src/components/token/TokenDragOverlay.js b/src/components/token/TokenDragOverlay.js index 06ef8a1..38d5195 100644 --- a/src/components/token/TokenDragOverlay.js +++ b/src/components/token/TokenDragOverlay.js @@ -79,7 +79,8 @@ function TokenDragOverlay({ ...mapState.tokens[mountedToken.id()], x: mountedToken.x() / mapWidth, y: mountedToken.y() / mapHeight, - lastEditedBy: userId, + lastModifiedBy: userId, + lastModified: Date.now(), }, }); } diff --git a/src/components/token/TokenMenu.js b/src/components/token/TokenMenu.js index 74ac4e2..fc901b6 100644 --- a/src/components/token/TokenMenu.js +++ b/src/components/token/TokenMenu.js @@ -1,5 +1,5 @@ -import React, { useEffect, useState } from "react"; -import { Box, Input, Slider, Flex, Text } from "theme-ui"; +import React, { useEffect, useState, useContext } from "react"; +import { Box, Input, Slider, Flex, Text, IconButton } from "theme-ui"; import MapMenu from "../map/MapMenu"; @@ -7,6 +7,13 @@ 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 defaultTokenMaxSize = 6; function TokenMenu({ isOpen, @@ -14,7 +21,10 @@ function TokenMenu({ tokenState, tokenImage, onTokenStateChange, + map, }) { + const { userId } = useContext(AuthContext); + const wasOpen = usePrevious(isOpen); const [tokenMaxSize, setTokenMaxSize] = useState(defaultTokenMaxSize); @@ -26,8 +36,8 @@ function TokenMenu({ // Update menu position if (tokenImage) { const imageRect = tokenImage.getClientRect(); - const map = document.querySelector(".map"); - const mapRect = map.getBoundingClientRect(); + const mapElement = document.querySelector(".map"); + const mapRect = mapElement.getBoundingClientRect(); // Center X for the menu which is 156px wide setMenuLeft(mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2); @@ -67,6 +77,18 @@ function TokenMenu({ }); } + function handleVisibleChange() { + onTokenStateChange({ + [tokenState.id]: { ...tokenState, visible: !tokenState.visible }, + }); + } + + function handleLockChange() { + onTokenStateChange({ + [tokenState.id]: { ...tokenState, locked: !tokenState.locked }, + }); + } + function handleModalContent(node) { if (node) { // Focus input @@ -76,8 +98,8 @@ function TokenMenu({ // Ensure menu is in bounds const nodeRect = node.getBoundingClientRect(); - const map = document.querySelector(".map"); - const mapRect = map.getBoundingClientRect(); + const mapElement = document.querySelector(".map"); + const mapRect = mapElement.getBoundingClientRect(); setMenuLeft((prevLeft) => Math.min( mapRect.right - nodeRect.width, @@ -203,6 +225,33 @@ function TokenMenu({ mr={1} /> + {/* Only show hide and lock token actions to map owners */} + {map && map.owner === userId && ( + + + {tokenState && tokenState.visible ? : } + + + {tokenState && tokenState.locked ? : } + + + )} ); diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js index 6a43587..bd40ae4 100644 --- a/src/components/token/Tokens.js +++ b/src/components/token/Tokens.js @@ -31,8 +31,11 @@ function Tokens({ onMapTokenStateCreate }) { statuses: [], x: token.x, y: token.y, - lastEditedBy: userId, + lastModifiedBy: userId, + lastModified: Date.now(), rotation: 0, + locked: false, + visible: true, }); } } diff --git a/src/database.js b/src/database.js index 56ad355..b171650 100644 --- a/src/database.js +++ b/src/database.js @@ -155,6 +155,23 @@ function loadVersions(db) { map.snapToGrid = true; }); }); + // v1.5.1 - Added lock, visibility and modified to tokens + db.version(9) + .stores({}) + .upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let id in state.tokens) { + state.tokens[id].lastModifiedBy = state.tokens[id].lastEditedBy; + delete state.tokens[id].lastEditedBy; + state.tokens[id].lastModified = Date.now(); + state.tokens[id].locked = false; + state.tokens[id].visible = true; + } + }); + }); } // Get the dexie database used in DatabaseContext diff --git a/src/icons/TokenHideIcon.js b/src/icons/TokenHideIcon.js new file mode 100644 index 0000000..3ab7dd3 --- /dev/null +++ b/src/icons/TokenHideIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function TokenHideIcon() { + return ( + + + + + ); +} + +export default TokenHideIcon; diff --git a/src/icons/TokenLockIcon.js b/src/icons/TokenLockIcon.js new file mode 100644 index 0000000..c4785d9 --- /dev/null +++ b/src/icons/TokenLockIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function TokenLockIcon() { + return ( + + + + + ); +} + +export default TokenLockIcon; diff --git a/src/icons/TokenShowIcon.js b/src/icons/TokenShowIcon.js new file mode 100644 index 0000000..b6c6792 --- /dev/null +++ b/src/icons/TokenShowIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function TokenShowIcon() { + return ( + + + + + ); +} + +export default TokenShowIcon; diff --git a/src/icons/TokenUnlockIcon.js b/src/icons/TokenUnlockIcon.js new file mode 100644 index 0000000..23133e6 --- /dev/null +++ b/src/icons/TokenUnlockIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function TokenUnlockIcon() { + return ( + + + {" "} + + ); +} + +export default TokenUnlockIcon;