From d3a711022c5c33a6d0dc4bf1f8287a0457a25220 Mon Sep 17 00:00:00 2001
From: Jeremie Pardou <571533+jrmi@users.noreply.github.com>
Date: Sun, 18 Feb 2024 22:12:28 +0100
Subject: [PATCH] Fix sync issues and click event for firefox
---
package-lock.json | 14 +-
package.json | 2 +-
.../AdvancedImage/AdvancedImage.jsx | 19 +-
.../CheckerBoard/CheckerBoard.jsx | 19 +-
src/gameComponents/Image/Image.jsx | 19 +-
src/gameComponents/Zone/Zone.jsx | 19 +-
src/gameComponents/useGameItemActions.jsx | 310 ++++++++++--------
src/ui/FixedButton.jsx | 50 +++
src/ui/NavButton.jsx | 6 +-
src/ui/Touch.jsx | 3 +-
src/users/UserCircle.jsx | 5 +-
src/utils/index.js | 10 +
src/utils/item.js | 21 +-
src/views/BoardView/SelectedItemsPane.jsx | 5 +-
14 files changed, 324 insertions(+), 178 deletions(-)
create mode 100644 src/ui/FixedButton.jsx
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 (
-
+
);
}
)}