Skip to content

Commit 835f0bd

Browse files
authored
Merge pull request #184 from boostcampwm-2024/feature-fe-#183
다른 사용자의 커서들을 표시
2 parents 0362bbe + 94498df commit 835f0bd

File tree

4 files changed

+162
-2
lines changed

4 files changed

+162
-2
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Panel } from "@xyflow/react";
2+
import { useReactFlow } from "@xyflow/react";
3+
import { type AwarenessState } from "@/hooks/useCursor";
4+
import Cursor from "./cursor";
5+
import { useMemo } from "react";
6+
7+
interface CollaborativeCursorsProps {
8+
cursors: Map<number, AwarenessState>;
9+
}
10+
11+
export function CollaborativeCursors({ cursors }: CollaborativeCursorsProps) {
12+
const { flowToScreenPosition } = useReactFlow();
13+
14+
const validCursors = useMemo(
15+
() => Array.from(cursors.values()).filter((cursor) => cursor.cursor),
16+
[cursors],
17+
);
18+
19+
return (
20+
<Panel>
21+
{validCursors.map((cursor) => (
22+
<Cursor
23+
key={cursor.clientId}
24+
coors={flowToScreenPosition({
25+
x: cursor.cursor!.x,
26+
y: cursor.cursor!.y,
27+
})}
28+
color={cursor.color}
29+
/>
30+
))}
31+
</Panel>
32+
);
33+
}

frontend/src/components/canvas/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { cn } from "@/lib/utils";
2525
import { useQueryClient } from "@tanstack/react-query";
2626
import useYDocStore from "@/store/useYDocStore";
2727

28+
import { useCollaborativeCursors } from "@/hooks/useCursor";
29+
import { CollaborativeCursors } from "../CursorView";
30+
2831
const proOptions = { hideAttribution: true };
2932

3033
interface CanvasProps {
@@ -39,6 +42,12 @@ function Flow({ className }: CanvasProps) {
3942

4043
const { ydoc } = useYDocStore();
4144

45+
const { cursors, handleMouseMove, handleNodeDrag, handleMouseLeave } =
46+
useCollaborativeCursors({
47+
ydoc,
48+
roomName: "flow-room",
49+
});
50+
4251
const provider = useRef<WebsocketProvider>();
4352
const existingPageIds = useRef(new Set<string>());
4453

@@ -214,12 +223,14 @@ function Flow({ className }: CanvasProps) {
214223
const nodeTypes = useMemo(() => ({ note: NoteNode }), []);
215224

216225
return (
217-
<div className={cn("", className)}>
226+
<div className={cn("", className)} onMouseMove={handleMouseMove}>
218227
<ReactFlow
219228
nodes={nodes}
220229
edges={edges}
221230
onNodesChange={handleNodesChange}
222231
onEdgesChange={handleEdgesChange}
232+
onMouseLeave={handleMouseLeave}
233+
onNodeDrag={handleNodeDrag}
223234
onConnect={onConnect}
224235
proOptions={proOptions}
225236
nodeTypes={nodeTypes}
@@ -229,6 +240,7 @@ function Flow({ className }: CanvasProps) {
229240
<Controls />
230241
<MiniMap />
231242
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
243+
<CollaborativeCursors cursors={cursors} />
232244
</ReactFlow>
233245
</div>
234246
);

frontend/src/components/cursor/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function Cursor({ coors, color = "#ffb8b9" }: CursorProps) {
3232
d="M2.40255 5.31234C1.90848 3.6645 3.58743 2.20312 5.15139 2.91972L90.0649 41.8264C91.7151 42.5825 91.5858 44.9688 89.8637 45.5422L54.7989 57.2186C53.3211 57.7107 52.0926 58.7582 51.3731 60.1397L33.0019 95.4124C32.1726 97.0047 29.8279 96.7826 29.3124 95.063L2.40255 5.31234Z"
3333
fill={color}
3434
stroke="black"
35-
stroke-width="4"
35+
strokeWidth="4"
3636
/>
3737
</svg>
3838
</motion.div>

frontend/src/hooks/useCursor.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { useReactFlow, type XYPosition } from "@xyflow/react";
2+
import * as Y from "yjs";
3+
import { useCallback, useEffect, useRef, useState } from "react";
4+
import { WebsocketProvider } from "y-websocket";
5+
6+
const CURSOR_COLORS = [
7+
"#7d7b94",
8+
"#41c76d",
9+
"#f86e7e",
10+
"#f6b8b8",
11+
"#f7d353",
12+
"#3b5bf7",
13+
"#59cbf7",
14+
] as const;
15+
16+
export interface AwarenessState {
17+
cursor: XYPosition | null;
18+
color: string;
19+
clientId: number;
20+
}
21+
22+
interface CollaborativeCursorsProps {
23+
ydoc: Y.Doc;
24+
roomName?: string;
25+
}
26+
27+
export function useCollaborativeCursors({
28+
ydoc,
29+
roomName = "cursor-room",
30+
}: CollaborativeCursorsProps) {
31+
const flowInstance = useReactFlow();
32+
const provider = useRef<WebsocketProvider>();
33+
const [cursors, setCursors] = useState<Map<number, AwarenessState>>(
34+
new Map(),
35+
);
36+
const clientId = useRef<number | null>(null);
37+
const userColor = useRef(
38+
CURSOR_COLORS[Math.floor(Math.random() * CURSOR_COLORS.length)],
39+
);
40+
41+
useEffect(() => {
42+
const wsProvider = new WebsocketProvider(
43+
import.meta.env.VITE_WS_URL,
44+
roomName,
45+
ydoc,
46+
);
47+
48+
provider.current = wsProvider;
49+
clientId.current = wsProvider.awareness.clientID;
50+
51+
wsProvider.awareness.setLocalState({
52+
cursor: null,
53+
color: userColor.current,
54+
clientId: wsProvider.awareness.clientID,
55+
});
56+
57+
wsProvider.awareness.on("change", () => {
58+
const states = new Map(
59+
Array.from(
60+
wsProvider.awareness.getStates() as Map<number, AwarenessState>,
61+
).filter(
62+
([key, state]) => key !== clientId.current && state.cursor !== null,
63+
),
64+
);
65+
setCursors(states);
66+
});
67+
68+
return () => {
69+
wsProvider.destroy();
70+
};
71+
}, [ydoc, roomName]);
72+
73+
const updateCursorPosition = useCallback(
74+
(x: number | null, y: number | null) => {
75+
if (!provider.current?.awareness) return;
76+
77+
const cursor =
78+
x !== null && y !== null
79+
? flowInstance?.screenToFlowPosition({ x, y })
80+
: null;
81+
82+
provider.current.awareness.setLocalState({
83+
cursor,
84+
color: userColor.current,
85+
clientId: provider.current.awareness.clientID,
86+
});
87+
},
88+
[flowInstance],
89+
);
90+
91+
const handleMouseMove = useCallback(
92+
(e: React.MouseEvent) => {
93+
updateCursorPosition(e.clientX, e.clientY);
94+
},
95+
[updateCursorPosition],
96+
);
97+
98+
const handleNodeDrag = useCallback(
99+
(e: React.MouseEvent) => {
100+
updateCursorPosition(e.clientX, e.clientY);
101+
},
102+
[updateCursorPosition],
103+
);
104+
105+
const handleMouseLeave = useCallback(() => {
106+
updateCursorPosition(null, null);
107+
}, [updateCursorPosition]);
108+
109+
return {
110+
cursors,
111+
handleMouseMove,
112+
handleNodeDrag,
113+
handleMouseLeave,
114+
};
115+
}

0 commit comments

Comments
 (0)