diff --git a/package-lock.json b/package-lock.json index ab9b8f5d..c2206911 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "react-icons": "^4.8.0", "react-query": "^3.39.3", "react-router-dom": "^6.8.0", - "react-sync-board": "^1.2.3", + "react-sync-board": "^1.2.4", "react-toastify": "^6.1.0", "recoil": "^0.7.4", "socket.io-client": "^4.1.2", @@ -9189,9 +9189,9 @@ } }, "node_modules/react-sync-board": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-1.2.3.tgz", - "integrity": "sha512-ZFMU+/41QGc4evIvTXy+fNSTpWSyZZvWhyvs0/TCmIUTVtfavJjK1y0ssdIQ5ElZM8H0jsx9QZbLXRG/fa0nrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-1.2.4.tgz", + "integrity": "sha512-J0MVWc0DWFgIbhPA65j87GYXOM+dOzIpgkjJ7TVxB/iOfzuj6YWKiDbyyX0edbVAAZY79LPfTwN3eLeFvFJwcw==", "dependencies": { "@react-hookz/web": "^22.0.0", "color2k": "^2.0.0", @@ -17857,9 +17857,9 @@ } }, "react-sync-board": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-1.2.3.tgz", - "integrity": "sha512-ZFMU+/41QGc4evIvTXy+fNSTpWSyZZvWhyvs0/TCmIUTVtfavJjK1y0ssdIQ5ElZM8H0jsx9QZbLXRG/fa0nrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-1.2.4.tgz", + "integrity": "sha512-J0MVWc0DWFgIbhPA65j87GYXOM+dOzIpgkjJ7TVxB/iOfzuj6YWKiDbyyX0edbVAAZY79LPfTwN3eLeFvFJwcw==", "requires": { "@react-hookz/web": "^22.0.0", "color2k": "^2.0.0", diff --git a/package.json b/package.json index 57991a98..fdcb3599 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "react-icons": "^4.8.0", "react-query": "^3.39.3", "react-router-dom": "^6.8.0", - "react-sync-board": "^1.2.3", + "react-sync-board": "^1.2.4", "react-toastify": "^6.1.0", "recoil": "^0.7.4", "socket.io-client": "^4.1.2", diff --git a/src/gameComponents/AdvancedImage/AdvancedImage.jsx b/src/gameComponents/AdvancedImage/AdvancedImage.jsx index 745cc640..8ee3f1bc 100644 --- a/src/gameComponents/AdvancedImage/AdvancedImage.jsx +++ b/src/gameComponents/AdvancedImage/AdvancedImage.jsx @@ -129,17 +129,24 @@ const AdvancedImage = ({ const onPlaceItem = React.useCallback( (itemIds) => { - setState((item) => ({ - ...item, - linkedItems: getHeldItems({ + setState((item) => { + const newLinkedItems = getHeldItems({ element: wrapperRef.current, currentItemId, - linkedItemIds: item.linkedItems, + currentLinkedItemIds: item.linkedItems, itemList: getItemList(), itemIds, shouldHoldItems: item.holdItems, - }), - })); + }); + + if (item.linkedItems !== newLinkedItems) { + return { + linkedItems: newLinkedItems, + }; + } + + return {}; + }, true); }, [currentItemId, getItemList, setState] ); diff --git a/src/gameComponents/CheckerBoard/CheckerBoard.jsx b/src/gameComponents/CheckerBoard/CheckerBoard.jsx index 7f7741c2..fe1c762c 100644 --- a/src/gameComponents/CheckerBoard/CheckerBoard.jsx +++ b/src/gameComponents/CheckerBoard/CheckerBoard.jsx @@ -37,17 +37,24 @@ const CheckerBoard = ({ const onPlaceItem = React.useCallback( (itemIds) => { - setState((item) => ({ - ...item, - linkedItems: getHeldItems({ + setState((item) => { + const newLinkedItems = getHeldItems({ element: wrapperRef.current, currentItemId, - linkedItemIds: item.linkedItems, + currentLinkedItemIds: item.linkedItems, itemList: getItemList(), itemIds, shouldHoldItems: item.holdItems, - }), - })); + }); + + if (item.linkedItems !== newLinkedItems) { + return { + linkedItems: newLinkedItems, + }; + } + + return {}; + }, true); }, [currentItemId, getItemList, setState] ); diff --git a/src/gameComponents/Image/Image.jsx b/src/gameComponents/Image/Image.jsx index 080d3ffb..a9feb9da 100644 --- a/src/gameComponents/Image/Image.jsx +++ b/src/gameComponents/Image/Image.jsx @@ -122,17 +122,24 @@ const Image = ({ const onPlaceItem = React.useCallback( (itemIds) => { - setState((item) => ({ - ...item, - linkedItems: getHeldItems({ + setState((item) => { + const newLinkedItems = getHeldItems({ element: wrapperRef.current, currentItemId, - linkedItemIds: item.linkedItems, + currentLinkedItemIds: item.linkedItems, itemList: getItemList(), itemIds, shouldHoldItems: item.holdItems, - }), - })); + }); + + if (item.linkedItems !== newLinkedItems) { + return { + linkedItems: newLinkedItems, + }; + } + + return {}; + }, true); }, [currentItemId, getItemList, setState] ); diff --git a/src/gameComponents/Zone/Zone.jsx b/src/gameComponents/Zone/Zone.jsx index 70a1ca0f..86b65da6 100644 --- a/src/gameComponents/Zone/Zone.jsx +++ b/src/gameComponents/Zone/Zone.jsx @@ -62,17 +62,24 @@ const Zone = ({ const onInsideItem = React.useCallback( (itemIds) => { - setState((item) => ({ - ...item, - linkedItems: getHeldItems({ + setState((item) => { + const newLinkedItems = getHeldItems({ element: zoneRef.current, currentItemId, - linkedItemIds: item.linkedItems, + currentLinkedItemIds: item.linkedItems, itemList: getItemList(), itemIds, shouldHoldItems: item.holdItems, - }), - })); + }); + + if (item.linkedItems !== newLinkedItems) { + return { + linkedItems: newLinkedItems, + }; + } + + return {}; + }, true); const addedItems = Object.entries( areItemsInside(zoneRef.current, itemIds) diff --git a/src/gameComponents/useGameItemActions.jsx b/src/gameComponents/useGameItemActions.jsx index 3136bafa..44431788 100644 --- a/src/gameComponents/useGameItemActions.jsx +++ b/src/gameComponents/useGameItemActions.jsx @@ -123,16 +123,19 @@ export const useGameItemActions = () => { let newY = minMax.min.y + (minMax.max.y - minMax.min.y) / 2 - clientHeight / 2; - batchUpdateItems(ids, (item) => { - const newItem = { - ...item, - x: newX, - y: newY, - }; - newX += stackThickness; - newY -= stackThickness; - return newItem; - }); + batchUpdateItems( + ids, + (item) => { + const newItem = { + x: newX, + y: newY, + }; + newX += stackThickness; + newY -= stackThickness; + return newItem; + }, + true + ); }, [batchUpdateItems, getItemListOrSelected] ); @@ -157,16 +160,19 @@ export const useGameItemActions = () => { stackThickness = stackThicknessMin; } - batchUpdateItems(ids, (item) => { - const newItem = { - ...item, - x: newX, - y: newY, - }; - newX += stackThickness; - newY -= stackThickness; - return newItem; - }); + batchUpdateItems( + ids, + (item) => { + const newItem = { + x: newX, + y: newY, + }; + newX += stackThickness; + newY -= stackThickness; + return newItem; + }, + true + ); }, [batchUpdateItems, getItemListOrSelected] ); @@ -179,16 +185,19 @@ export const useGameItemActions = () => { let { x: newX, y: newY } = items[0]; - batchUpdateItems(ids, (item) => { - const { clientWidth } = getItemElement(item.id); - const newItem = { - ...item, - x: newX, - y: newY, - }; - newX += clientWidth + gapBetweenItems; - return newItem; - }); + batchUpdateItems( + ids, + (item) => { + const { clientWidth } = getItemElement(item.id); + const newItem = { + x: newX, + y: newY, + }; + newX += clientWidth + gapBetweenItems; + return newItem; + }, + true + ); }, [getItemListOrSelected, batchUpdateItems] ); @@ -207,40 +216,46 @@ export const useGameItemActions = () => { let currentColumn = 1; - batchUpdateItems(ids, (item) => { - const { clientWidth, clientHeight } = getItemElement(item.id); - const newItem = { - ...item, - x: newX, - y: newY, - }; - newX += clientWidth + gapBetweenItems; - currentColumn += 1; - if (currentColumn > numberOfColumns) { - currentColumn = 1; - newX = items[0].x; - newY += clientHeight + gapBetweenItems; - } - return newItem; - }); + batchUpdateItems( + ids, + (item) => { + const { clientWidth, clientHeight } = getItemElement(item.id); + const newItem = { + x: newX, + y: newY, + }; + newX += clientWidth + gapBetweenItems; + currentColumn += 1; + if (currentColumn > numberOfColumns) { + currentColumn = 1; + newX = items[0].x; + newY += clientHeight + gapBetweenItems; + } + return newItem; + }, + true + ); }, [getItemListOrSelected, batchUpdateItems] ); const snapToPoint = React.useCallback( async (itemIds, { x, y } = {}) => { - batchUpdateItems(itemIds, (item) => { - const { clientWidth, clientHeight } = getItemElement(item.id); - let newX = x - clientWidth / 2; - let newY = y - clientHeight / 2; - - const newItem = { - ...item, - x: newX, - y: newY, - }; - return newItem; - }); + batchUpdateItems( + itemIds, + (item) => { + const { clientWidth, clientHeight } = getItemElement(item.id); + let newX = x - clientWidth / 2; + let newY = y - clientHeight / 2; + + const newItem = { + x: newX, + y: newY, + }; + return newItem; + }, + true + ); }, [batchUpdateItems] ); @@ -257,13 +272,11 @@ export const useGameItemActions = () => { switch (item.type) { case "dice": return { - ...item, value: randInt(0, (item.side || 6) - 1), }; case "diceImage": case "imageSequence": return { - ...item, value: randInt(0, item.images.length - 1), }; case "advancedImage": { @@ -273,17 +286,21 @@ export const useGameItemActions = () => { } return layer; }); - return { ...item, layers: newLayers }; + return { layers: newLayers }; } default: - return item; + return {}; } }; const simulateRoll = (nextTimeout) => { - batchUpdateItems(ids, (item) => { - return randomizeValue(item); - }); + batchUpdateItems( + ids, + (item) => { + return randomizeValue(item); + }, + true + ); if (nextTimeout < 300) { setTimeout( () => simulateRoll(nextTimeout + randInt(10, 50)), @@ -312,44 +329,44 @@ export const useGameItemActions = () => { if (step > 0) { return { - ...item, value: (value + step) % max, }; } else { const newValue = value + step; return { - ...item, value: newValue >= 0 ? newValue : max + newValue, }; } }; - batchUpdateItems(ids, (item) => { - switch (item.type) { - case "dice": - return stepItem(item, item.side || 6); - case "diceImage": - case "imageSequence": - return stepItem(item, item.images.length); - case "counter": - return { - ...item, - value: isNaN(item.value) ? 0 : item.value + step, - }; - case "advancedImage": - return { - ...item, - layers: item.layers.map((layer, index) => { - if (index === layerToUpdate) { - return stepItem(layer, layer.images.length); - } - return layer; - }), - }; - default: - return item; - } - }); + batchUpdateItems( + ids, + (item) => { + switch (item.type) { + case "dice": + return stepItem(item, item.side || 6); + case "diceImage": + case "imageSequence": + return stepItem(item, item.images.length); + case "counter": + return { + value: isNaN(item.value) ? 0 : item.value + step, + }; + case "advancedImage": + return { + layers: item.layers.map((layer, index) => { + if (index === layerToUpdate) { + return stepItem(layer, layer.images.length); + } + return layer; + }), + }; + default: + return item; + } + }, + true + ); }, [batchUpdateItems, getItemListOrSelected] ); @@ -376,11 +393,15 @@ export const useGameItemActions = () => { const maxRotate = maxRotateCount || Math.round(360 / angle); - batchUpdateItems(ids, (item) => { - const rotation = - ((item.rotation || 0) + angle * randInt(0, maxRotate)) % 360; - return { ...item, rotation }; - }); + batchUpdateItems( + ids, + (item) => { + const rotation = + ((item.rotation || 0) + angle * randInt(0, maxRotate)) % 360; + return { rotation }; + }, + true + ); }, [getItemListOrSelected, batchUpdateItems] ); @@ -399,11 +420,14 @@ export const useGameItemActions = () => { tap = false; } - batchUpdateItems(ids, (item) => ({ - ...item, - tapped: tap, - rotation: tap ? (item.rotation || 0) + 90 : (item.rotation || 0) - 90, - })); + batchUpdateItems( + ids, + (item) => ({ + tapped: tap, + rotation: tap ? (item.rotation || 0) + 90 : (item.rotation || 0) - 90, + }), + true + ); }, [getItemListOrSelected, batchUpdateItems] ); @@ -413,10 +437,13 @@ export const useGameItemActions = () => { async (itemIds) => { const [ids] = await getItemListOrSelected(itemIds); - batchUpdateItems(ids, (item) => ({ - ...item, - locked: !item.locked, - })); + batchUpdateItems( + ids, + (item) => ({ + locked: !item.locked, + }), + true + ); // Help user on first lock if (isFirstLock) { @@ -449,14 +476,17 @@ export const useGameItemActions = () => { }) .map(({ id }) => id); - batchUpdateItems(itemIdsToFlip, (item) => ({ - ...item, - flipped: flip, - unflippedFor: - !Array.isArray(item.unflippedFor) || item.unflippedFor.length > 0 - ? null - : item.unflippedFor, - })); + batchUpdateItems( + itemIdsToFlip, + (item) => ({ + flipped: flip, + unflippedFor: + !Array.isArray(item.unflippedFor) || item.unflippedFor.length > 0 + ? null + : item.unflippedFor, + }), + true + ); if (reverseOrder) { reverseItemsOrder(itemIdsToFlip); } @@ -487,10 +517,13 @@ export const useGameItemActions = () => { async (itemIds, { angle }) => { const [ids] = await getItemListOrSelected(itemIds); - batchUpdateItems(ids, (item) => ({ - ...item, - rotation: (item.rotation || 0) + angle, - })); + batchUpdateItems( + ids, + (item) => ({ + rotation: (item.rotation || 0) + angle, + }), + true + ); }, [getItemListOrSelected, batchUpdateItems] ); @@ -518,26 +551,29 @@ export const useGameItemActions = () => { }) .map(({ id }) => id); - batchUpdateItems(itemIdsToFlip, (item) => { - let { unflippedFor = [] } = item; + batchUpdateItems( + itemIdsToFlip, + (item) => { + let { unflippedFor = [] } = item; - if (!Array.isArray(item.unflippedFor)) { - unflippedFor = []; - } - const isFlippedFor = unflippedFor.includes(currentUser.uid); + if (!Array.isArray(item.unflippedFor)) { + unflippedFor = []; + } + const isFlippedFor = unflippedFor.includes(currentUser.uid); - if (flipSelf && !isFlippedFor) { - unflippedFor = [...unflippedFor, currentUser.uid]; - } - if (!flipSelf && isFlippedFor) { - unflippedFor = unflippedFor.filter((id) => id !== currentUser.uid); - } - return { - ...item, - flipped: true, - unflippedFor, - }; - }); + if (flipSelf && !isFlippedFor) { + unflippedFor = [...unflippedFor, currentUser.uid]; + } + if (!flipSelf && isFlippedFor) { + unflippedFor = unflippedFor.filter((id) => id !== currentUser.uid); + } + return { + flipped: true, + unflippedFor, + }; + }, + true + ); if (itemIdsToFlip.length) { playAudio(flipAudio, 0.2); diff --git a/src/ui/FixedButton.jsx b/src/ui/FixedButton.jsx new file mode 100644 index 00000000..ae3874b8 --- /dev/null +++ b/src/ui/FixedButton.jsx @@ -0,0 +1,50 @@ +import React from "react"; + +import { useEventListener } from "@react-hookz/web/esm/useEventListener"; + +/** + * Button to fix the firefox bug when the click event is triggered even if you don't + * want to. + */ +const FixedButton = ({ children, onClick, disabled, className, title }) => { + const mouseDownedRef = React.useRef(false); + const buttonRef = React.useRef(); + + // Fix while firefox triggers click event even if the mousedown event wasn't here + useEventListener(window, "mouseup", () => { + mouseDownedRef.current = false; + }); + + const handleClick = React.useCallback( + (e) => { + if (!mouseDownedRef.current) { + return; + } + mouseDownedRef.current = false; + if (disabled) { + return; + } + if (onClick) { + onClick(e); + } + }, + [disabled, onClick] + ); + + return ( + + ); +}; + +export default FixedButton; diff --git a/src/ui/NavButton.jsx b/src/ui/NavButton.jsx index 72c7363f..b0bbe36b 100644 --- a/src/ui/NavButton.jsx +++ b/src/ui/NavButton.jsx @@ -2,7 +2,11 @@ import React from "react"; import styled from "styled-components"; import { useNavigate } from "react-router-dom"; -const StyledNavButton = styled.button` +import { useEventListener } from "@react-hookz/web/esm/useEventListener"; +import { insideElement } from "../utils"; +import FixedButton from "./FixedButton"; + +const StyledNavButton = styled(FixedButton)` display: block; background-color: transparent; padding: 4px; diff --git a/src/ui/Touch.jsx b/src/ui/Touch.jsx index 5a004793..069d7b7d 100644 --- a/src/ui/Touch.jsx +++ b/src/ui/Touch.jsx @@ -1,6 +1,7 @@ import React from "react"; import styled from "styled-components"; import { useNavigate } from "react-router-dom"; +import FixedButton from "./FixedButton"; const StyledWrapper = styled.div` display: flex; @@ -16,7 +17,7 @@ const StyledWrapper = styled.div` } `; -const StyledButton = styled.div.attrs(({ active }) => ({ +const StyledButton = styled(FixedButton).attrs(({ active }) => ({ className: active ? "active" : "", }))` border-radius: 100%; diff --git a/src/users/UserCircle.jsx b/src/users/UserCircle.jsx index 643ff6bb..adca462b 100644 --- a/src/users/UserCircle.jsx +++ b/src/users/UserCircle.jsx @@ -1,8 +1,11 @@ import React from "react"; import styled from "styled-components"; +import FixedButton from "../ui/FixedButton"; -const StyledUserCircle = styled.div` +const StyledUserCircle = styled(FixedButton)` + margin: 0px; + padding: 0px; background-color: ${({ color }) => color}; width: 30px; min-width: 30px; diff --git a/src/utils/index.js b/src/utils/index.js index eb5c61dc..61464b45 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -31,6 +31,16 @@ export const insideClass = (element, className) => { return insideClass(element.parentNode, className); }; +export const insideElement = (wrapperElement, element) => { + if (element === wrapperElement) { + return true; + } + if (!element.parentNode) { + return false; + } + return insideClass(wrapperElement, element.parentNode); +}; + /** * Shuffles array in place. * @param {Array} a An array containing the items. diff --git a/src/utils/item.js b/src/utils/item.js index 91a66e1f..c6e162b2 100644 --- a/src/utils/item.js +++ b/src/utils/item.js @@ -108,7 +108,7 @@ export const availableItemVisitor = async (items, callback) => { export const getHeldItems = ({ element, currentItemId, - linkedItemIds, + currentLinkedItemIds, itemList, itemIds, shouldHoldItems, @@ -124,12 +124,25 @@ export const getHeldItems = ({ return result; }) .map(({ id }) => id); - return Object.entries( - areItemsInside(element, afterItemIds, linkedItemIds || [], true) + const newHeldItems = Object.entries( + areItemsInside(element, afterItemIds, currentLinkedItemIds || [], true) ) .filter(([, { inside }]) => inside) .map(([itemId]) => itemId); + if ( + currentLinkedItemIds.length !== newHeldItems.length || + !currentLinkedItemIds.every((itemId) => newHeldItems.includes(itemId)) + ) { + return newHeldItems; + } } else { - return []; + if ( + !Array.isArray(currentLinkedItemIds) || + currentLinkedItemIds.length !== 0 + ) { + return []; + } } + + return currentLinkedItemIds; }; diff --git a/src/views/BoardView/SelectedItemsPane.jsx b/src/views/BoardView/SelectedItemsPane.jsx index fd1ca1b3..a16c3470 100644 --- a/src/views/BoardView/SelectedItemsPane.jsx +++ b/src/views/BoardView/SelectedItemsPane.jsx @@ -11,6 +11,7 @@ import { useItemActions, } from "react-sync-board"; import useGameItemActions from "../../gameComponents/useGameItemActions"; +import FixedButton from "../../ui/FixedButton"; const ActionPaneWrapper = styled.div` pointer-events: none; @@ -178,14 +179,14 @@ const SelectedItemsPane = ({ ({ label, action, edit: onlyEdit, shortcut, icon: Icon, uid }) => { if (onlyEdit && !showEdit) return null; return ( - + ); } )}