sets up liveblock for collaboration feat
This commit is contained in:
@@ -22,3 +22,5 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
.env
|
||||
Generated
+73
@@ -8,6 +8,9 @@
|
||||
"name": "lego-builder",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@liveblocks/client": "^1.2.2",
|
||||
"@liveblocks/react": "^1.2.2",
|
||||
"@liveblocks/zustand": "^1.2.2",
|
||||
"@radix-ui/colors": "^2.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
@@ -981,6 +984,44 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@liveblocks/client": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.2.2.tgz",
|
||||
"integrity": "sha512-cav7NIoZNGmOC07p/spaCiNwLL/eeKKTAscLb2wN5myXG+xWlf6sj6zIxMOSA1eTDcDDvDMsnkLUviUUK0QGWA==",
|
||||
"dependencies": {
|
||||
"@liveblocks/core": "1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@liveblocks/core": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.2.2.tgz",
|
||||
"integrity": "sha512-1WALetZ600EN3D4VaSpXse2Plp5+7os78B1kAANlOmf/NtiQKIoEBE40xYBVd4QAJgX6YIYGjOsQQja0CUqs7g=="
|
||||
},
|
||||
"node_modules/@liveblocks/react": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-1.2.2.tgz",
|
||||
"integrity": "sha512-OEPnS4C+cl1RgKOBs8RSYct6EbkzhQcSALIO1j+CSwJqYMy9vJxSd8nrqRqn+Nz9Uanmrgp01HTGrbHw+vp0gg==",
|
||||
"dependencies": {
|
||||
"@liveblocks/client": "1.2.2",
|
||||
"@liveblocks/core": "1.2.2",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@liveblocks/zustand": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/zustand/-/zustand-1.2.2.tgz",
|
||||
"integrity": "sha512-F8xTtR3yE2SJ5FYHMpdAu6mheSBubjc4AekNaoePg2uV8BfZljNT1VrU+1SKCCEddV1e96OOYQqP+iBJcryYhg==",
|
||||
"dependencies": {
|
||||
"@liveblocks/client": "1.2.2",
|
||||
"@liveblocks/core": "1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zustand": "^4.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mediapipe/tasks-vision": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz",
|
||||
@@ -6163,6 +6204,38 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"@liveblocks/client": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-1.2.2.tgz",
|
||||
"integrity": "sha512-cav7NIoZNGmOC07p/spaCiNwLL/eeKKTAscLb2wN5myXG+xWlf6sj6zIxMOSA1eTDcDDvDMsnkLUviUUK0QGWA==",
|
||||
"requires": {
|
||||
"@liveblocks/core": "1.2.2"
|
||||
}
|
||||
},
|
||||
"@liveblocks/core": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-1.2.2.tgz",
|
||||
"integrity": "sha512-1WALetZ600EN3D4VaSpXse2Plp5+7os78B1kAANlOmf/NtiQKIoEBE40xYBVd4QAJgX6YIYGjOsQQja0CUqs7g=="
|
||||
},
|
||||
"@liveblocks/react": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-1.2.2.tgz",
|
||||
"integrity": "sha512-OEPnS4C+cl1RgKOBs8RSYct6EbkzhQcSALIO1j+CSwJqYMy9vJxSd8nrqRqn+Nz9Uanmrgp01HTGrbHw+vp0gg==",
|
||||
"requires": {
|
||||
"@liveblocks/client": "1.2.2",
|
||||
"@liveblocks/core": "1.2.2",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"@liveblocks/zustand": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@liveblocks/zustand/-/zustand-1.2.2.tgz",
|
||||
"integrity": "sha512-F8xTtR3yE2SJ5FYHMpdAu6mheSBubjc4AekNaoePg2uV8BfZljNT1VrU+1SKCCEddV1e96OOYQqP+iBJcryYhg==",
|
||||
"requires": {
|
||||
"@liveblocks/client": "1.2.2",
|
||||
"@liveblocks/core": "1.2.2"
|
||||
}
|
||||
},
|
||||
"@mediapipe/tasks-vision": {
|
||||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.2.tgz",
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@liveblocks/client": "^1.2.2",
|
||||
"@liveblocks/react": "^1.2.2",
|
||||
"@liveblocks/zustand": "^1.2.2",
|
||||
"@radix-ui/colors": "^2.0.1",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
|
||||
+19
@@ -1,7 +1,26 @@
|
||||
import "./App.css";
|
||||
import { useEffect } from "react";
|
||||
import { LegoRoom } from "./components/3D";
|
||||
import { useStore } from "./store";
|
||||
import { Loader } from "./components/UI/Loader";
|
||||
|
||||
const CONNECTED = "connected";
|
||||
|
||||
function App() {
|
||||
const enterRoom = useStore((state) => state.liveblocks.enterRoom);
|
||||
const leaveRoom = useStore((state) => state.liveblocks.leaveRoom);
|
||||
const status = useStore((state) => state.liveblocks.status);
|
||||
|
||||
useEffect(() => {
|
||||
enterRoom("lego-test-room");
|
||||
|
||||
return () => leaveRoom("lego-test-room", { bricks: [] });
|
||||
}, [enterRoom, leaveRoom]);
|
||||
|
||||
if (status !== CONNECTED) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<LegoRoom />
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable react/prop-types */
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import { useLayoutEffect, useMemo, useRef } from "react";
|
||||
import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
} from "react";
|
||||
import { useFrame, useThree } from "@react-three/fiber";
|
||||
import { DoubleSide, RawShaderMaterial, Color, Vector2 } from "three";
|
||||
|
||||
@@ -45,14 +51,13 @@ void main() {
|
||||
}
|
||||
`;
|
||||
|
||||
export const BorderPlane = ({
|
||||
planeSize = [10, 10],
|
||||
color = "#ffffff",
|
||||
...props
|
||||
}) => {
|
||||
export const BorderPlane = forwardRef(function BorderPlane(
|
||||
{ planeSize = [10, 10], color = "#ffffff", ...props },
|
||||
ref
|
||||
) {
|
||||
const { size } = useThree();
|
||||
|
||||
const ref = useRef();
|
||||
const mesh = useRef();
|
||||
|
||||
const uniforms = useMemo(() => {
|
||||
return {
|
||||
@@ -60,22 +65,22 @@ export const BorderPlane = ({
|
||||
uBorderColor: { value: new Color(0xffffff) },
|
||||
uBorderWidth: { value: 10 },
|
||||
uResolution: { value: new Vector2() },
|
||||
uSize: { value: new Vector2(20, 20) },
|
||||
uSize: { value: new Vector2(25, 25) },
|
||||
};
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
ref.current.material.uniforms.uResolution.value.x = size.width;
|
||||
ref.current.material.uniforms.uResolution.value.y = size.height;
|
||||
mesh.current.material.uniforms.uResolution.value.x = size.width;
|
||||
mesh.current.material.uniforms.uResolution.value.y = size.height;
|
||||
}, [size]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
ref.current.material.uniforms.uSize.value.x = planeSize[0];
|
||||
ref.current.material.uniforms.uSize.value.y = planeSize[1];
|
||||
mesh.current.material.uniforms.uSize.value.x = planeSize[0];
|
||||
mesh.current.material.uniforms.uSize.value.y = planeSize[1];
|
||||
}, [planeSize]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// ref.current.material.uniforms.uBorderColor.value = color;
|
||||
mesh.current.material.uniforms.uBorderColor.value.set(color);
|
||||
}, [color]);
|
||||
|
||||
const planeMaterial = useMemo(() => {
|
||||
@@ -94,14 +99,16 @@ export const BorderPlane = ({
|
||||
// planeMaterial.uniforms.uTime.value = elapsedTime;
|
||||
// });
|
||||
|
||||
useImperativeHandle(ref, () => mesh.current);
|
||||
|
||||
return (
|
||||
<mesh
|
||||
rotation={[-Math.PI / 2, 0, 0]}
|
||||
ref={ref}
|
||||
ref={mesh}
|
||||
material={planeMaterial}
|
||||
{...props}
|
||||
>
|
||||
<planeGeometry args={planeSize} />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -94,6 +94,9 @@ export const Brick = ({
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ type: "spring", stiffness: 250, duration: 2 }}
|
||||
userData={{
|
||||
uID,
|
||||
}}
|
||||
>
|
||||
<mesh
|
||||
castShadow
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable react/prop-types */
|
||||
import { useStore } from "../../store";
|
||||
import { memo, useRef, useEffect } from "react";
|
||||
import { BorderPlane } from "./BorderPlane";
|
||||
import { base } from "../../utils";
|
||||
|
||||
const Other = memo(function Other({ id, otherCurors }) {
|
||||
const room = useStore((state) => state.liveblocks.room);
|
||||
|
||||
const ref = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
room.subscribe("event", ({ event }) => {
|
||||
if (event.type === id) {
|
||||
const d = event.data;
|
||||
ref.current.position.x = d.x;
|
||||
ref.current.position.z = d.z;
|
||||
ref.current.position.y = d.y - 31 / 2;
|
||||
|
||||
ref.current.scale.set(d.w, d.d);
|
||||
}
|
||||
});
|
||||
}, [id, room]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BorderPlane planeSize={[25, 25]} ref={ref} color={"#ff0000"} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const Others = () => {
|
||||
const others = useStore((state) => state.liveblocks.others);
|
||||
|
||||
return (
|
||||
<>
|
||||
{others.map((user) => {
|
||||
return user.presence?.self ? (
|
||||
<Other key={user.presence.self.id} id={user.presence.self.id} />
|
||||
) : null;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -21,10 +21,10 @@ import {
|
||||
} from "../../utils";
|
||||
import { ChangeColor } from "./ChangeColor";
|
||||
import { useStore } from "../../store";
|
||||
import { BorderPlane } from "./BorderPlane";
|
||||
// import { BorderPlane } from "./BorderPlane";
|
||||
import { Others } from "./Others";
|
||||
|
||||
export const Scene = () => {
|
||||
// const [bricks, setBricks] = useState([]);
|
||||
const bricks = useStore((state) => state.bricks);
|
||||
const setBricks = useStore((state) => state.setBricks);
|
||||
|
||||
@@ -42,6 +42,10 @@ export const Scene = () => {
|
||||
const anchorZ = useStore((state) => state.anchorZ);
|
||||
const rotate = useStore((state) => state.rotate);
|
||||
const color = useStore((state) => state.color);
|
||||
|
||||
const room = useStore((state) => state.liveblocks.room);
|
||||
const self = useStore((state) => state.self);
|
||||
|
||||
// useAnchorShorcuts(anchorX, anchorZ, set);
|
||||
|
||||
// const setSelection = useStore(state => state.setSelectedBricks)
|
||||
@@ -143,6 +147,17 @@ export const Scene = () => {
|
||||
evenDepth ? base : base / 2
|
||||
)
|
||||
);
|
||||
|
||||
room.broadcastEvent({
|
||||
type: self.id,
|
||||
data: {
|
||||
x: brickCursorRef.current.position.x,
|
||||
y: brickCursorRef.current.position.y,
|
||||
z: brickCursorRef.current.position.z,
|
||||
w: width,
|
||||
d: depth,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onClick = (e) => {
|
||||
@@ -208,11 +223,7 @@ export const Scene = () => {
|
||||
dimensions={{ x: width, z: depth }}
|
||||
translation={{ x: anchorX, z: anchorZ }}
|
||||
/>
|
||||
{/* <BorderPlane
|
||||
planeSize={[25, 100]}
|
||||
color="#ff0000"
|
||||
position={[25 / 2, 0.1, 0]}
|
||||
/> */}
|
||||
<Others />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -36,6 +36,8 @@ export const BottomBar = () => {
|
||||
setSelection({});
|
||||
};
|
||||
|
||||
const clearBricks = useStore((state) => state.clearBricks);
|
||||
|
||||
return (
|
||||
<div className="BottomBar">
|
||||
<button className="Button violet">
|
||||
@@ -51,6 +53,9 @@ export const BottomBar = () => {
|
||||
<button onClick={deleteSelectedBricks} className="Button violet">
|
||||
<TrashIcon className="Icon" color="black" />
|
||||
</button>
|
||||
<button onClick={clearBricks} className="Button violet">
|
||||
<TrashIcon className="Icon" color="black" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
export const Loader = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "black",
|
||||
color: "white",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createClient } from "@liveblocks/client";
|
||||
import { createRoomContext } from "@liveblocks/react";
|
||||
|
||||
const client = createClient({
|
||||
publicApiKey: import.meta.env.VITE_LIVEBLOCKS_API_KEY,
|
||||
});
|
||||
|
||||
export const {
|
||||
suspense: {
|
||||
RoomProvider,
|
||||
useRoom,
|
||||
useMyPresence,
|
||||
useUpdateMyPresence,
|
||||
useSelf,
|
||||
useOthers,
|
||||
useOthersMapped,
|
||||
useOthersConnectionIds,
|
||||
useOther,
|
||||
useBroadcastEvent,
|
||||
useEventListener,
|
||||
useErrorListener,
|
||||
useStorage,
|
||||
useObject,
|
||||
useMap,
|
||||
useList,
|
||||
useBatch,
|
||||
useHistory,
|
||||
useUndo,
|
||||
useRedo,
|
||||
useCanUndo,
|
||||
useCanRedo,
|
||||
useMutation,
|
||||
},
|
||||
} = createRoomContext(client);
|
||||
+108
-35
@@ -1,45 +1,118 @@
|
||||
import { create } from "zustand";
|
||||
import { CREATE_MODE, defaultAnchor, defaultWidth } from "../utils";
|
||||
import { CREATE_MODE, defaultAnchor, defaultWidth, uID } from "../utils";
|
||||
import { createClient } from "@liveblocks/client";
|
||||
import { liveblocks } from "@liveblocks/zustand";
|
||||
|
||||
export const useStore = create((set) => ({
|
||||
mode: CREATE_MODE,
|
||||
setMode: (newMode) => set({ mode: newMode }),
|
||||
const client = createClient({
|
||||
publicApiKey: import.meta.env.VITE_LIVEBLOCKS_API_KEY,
|
||||
});
|
||||
|
||||
width: defaultWidth,
|
||||
setWidth: (newWidth) => set({ width: newWidth }),
|
||||
// export const useStore = create((set) => ({
|
||||
// mode: CREATE_MODE,
|
||||
// setMode: (newMode) => set({ mode: newMode }),
|
||||
|
||||
depth: defaultWidth,
|
||||
setDepth: (newDepth) => set({ depth: newDepth }),
|
||||
// width: defaultWidth,
|
||||
// setWidth: (newWidth) => set({ width: newWidth }),
|
||||
|
||||
anchorX: defaultAnchor,
|
||||
setAnchorX: (newAnchorPoint) => set({ anchorX: newAnchorPoint }),
|
||||
// depth: defaultWidth,
|
||||
// setDepth: (newDepth) => set({ depth: newDepth }),
|
||||
|
||||
anchorZ: defaultAnchor,
|
||||
setAnchorZ: (newAnchorPoint) => set({ anchorZ: newAnchorPoint }),
|
||||
// anchorX: defaultAnchor,
|
||||
// setAnchorX: (newAnchorPoint) => set({ anchorX: newAnchorPoint }),
|
||||
|
||||
rotate: false,
|
||||
setRotate: (bool) => set({ rotate: bool }),
|
||||
// anchorZ: defaultAnchor,
|
||||
// setAnchorZ: (newAnchorPoint) => set({ anchorZ: newAnchorPoint }),
|
||||
|
||||
color: "#ff0000",
|
||||
setColor: (newColor) => set({ color: newColor }),
|
||||
// rotate: false,
|
||||
// setRotate: (bool) => set({ rotate: bool }),
|
||||
|
||||
selectedBricks: [],
|
||||
setSelectedBricks: ({ object, shift }) =>
|
||||
set((state) => {
|
||||
if (object === undefined) return { selectedBricks: [] };
|
||||
else if (Array.isArray(object)) return { selectedBricks: object };
|
||||
else if (!shift)
|
||||
return state.selectedBricks[0] === object
|
||||
? { selectedBricks: [] }
|
||||
: { selectedBricks: [object] };
|
||||
else if (state.selectedBricks.includes(object))
|
||||
return {
|
||||
selectedBricks: state.selectedBricks.filter((o) => o !== object),
|
||||
};
|
||||
else return { selectedBricks: [object, ...state.selectedBricks] };
|
||||
// color: "#ff0000",
|
||||
// setColor: (newColor) => set({ color: newColor }),
|
||||
|
||||
// selectedBricks: [],
|
||||
// setSelectedBricks: ({ object, shift }) =>
|
||||
// set((state) => {
|
||||
// if (object === undefined) return { selectedBricks: [] };
|
||||
// else if (Array.isArray(object)) return { selectedBricks: object };
|
||||
// else if (!shift)
|
||||
// return state.selectedBricks[0] === object
|
||||
// ? { selectedBricks: [] }
|
||||
// : { selectedBricks: [object] };
|
||||
// else if (state.selectedBricks.includes(object))
|
||||
// return {
|
||||
// selectedBricks: state.selectedBricks.filter((o) => o !== object),
|
||||
// };
|
||||
// else return { selectedBricks: [object, ...state.selectedBricks] };
|
||||
// }),
|
||||
|
||||
// bricks: [],
|
||||
// setBricks: (getBricks) =>
|
||||
// set((state) => ({ bricks: getBricks(state.bricks) })),
|
||||
// clearBricks: () => set({ bricks: [] }),
|
||||
|
||||
// cursor: {
|
||||
// x: 0,
|
||||
// z: 0,
|
||||
// w: 0,
|
||||
// d: 0,
|
||||
// },
|
||||
|
||||
// setCursor: (newState) => set({ cursor: newState }),
|
||||
// }));
|
||||
|
||||
const id = uID();
|
||||
|
||||
export const useStore = create(
|
||||
liveblocks(
|
||||
(set) => ({
|
||||
mode: CREATE_MODE,
|
||||
setMode: (newMode) => set({ mode: newMode }),
|
||||
|
||||
width: defaultWidth,
|
||||
setWidth: (newWidth) => set({ width: newWidth }),
|
||||
|
||||
depth: defaultWidth,
|
||||
setDepth: (newDepth) => set({ depth: newDepth }),
|
||||
|
||||
anchorX: defaultAnchor,
|
||||
setAnchorX: (newAnchorPoint) => set({ anchorX: newAnchorPoint }),
|
||||
|
||||
anchorZ: defaultAnchor,
|
||||
setAnchorZ: (newAnchorPoint) => set({ anchorZ: newAnchorPoint }),
|
||||
|
||||
rotate: false,
|
||||
setRotate: (bool) => set({ rotate: bool }),
|
||||
|
||||
color: "#ff0000",
|
||||
setColor: (newColor) => set({ color: newColor }),
|
||||
|
||||
selectedBricks: [],
|
||||
setSelectedBricks: ({ object, shift }) =>
|
||||
set((state) => {
|
||||
if (object === undefined) return { selectedBricks: [] };
|
||||
else if (Array.isArray(object)) return { selectedBricks: object };
|
||||
else if (!shift)
|
||||
return state.selectedBricks[0] === object
|
||||
? { selectedBricks: [] }
|
||||
: { selectedBricks: [object] };
|
||||
else if (state.selectedBricks.includes(object))
|
||||
return {
|
||||
selectedBricks: state.selectedBricks.filter((o) => o !== object),
|
||||
};
|
||||
else return { selectedBricks: [object, ...state.selectedBricks] };
|
||||
}),
|
||||
|
||||
bricks: [],
|
||||
setBricks: (getBricks) =>
|
||||
set((state) => ({ bricks: getBricks(state.bricks) })),
|
||||
clearBricks: () => set({ bricks: [] }),
|
||||
|
||||
self: { id: id },
|
||||
}),
|
||||
|
||||
bricks: [],
|
||||
setBricks: (getBricks) =>
|
||||
set((state) => ({ bricks: getBricks(state.bricks) })),
|
||||
}));
|
||||
{
|
||||
client,
|
||||
presenceMapping: { self: true },
|
||||
storageMapping: { bricks: true },
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user