Skip to content

Commit

Permalink
Merge pull request #184 from boostcampwm-2024/feature-fe-#183
Browse files Browse the repository at this point in the history
다른 사용자의 커서들을 표시
  • Loading branch information
yewonJin authored Nov 18, 2024
2 parents 0362bbe + 94498df commit 835f0bd
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 2 deletions.
33 changes: 33 additions & 0 deletions frontend/src/components/CursorView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Panel } from "@xyflow/react";
import { useReactFlow } from "@xyflow/react";
import { type AwarenessState } from "@/hooks/useCursor";
import Cursor from "./cursor";
import { useMemo } from "react";

interface CollaborativeCursorsProps {
cursors: Map<number, AwarenessState>;
}

export function CollaborativeCursors({ cursors }: CollaborativeCursorsProps) {
const { flowToScreenPosition } = useReactFlow();

const validCursors = useMemo(
() => Array.from(cursors.values()).filter((cursor) => cursor.cursor),
[cursors],
);

return (
<Panel>
{validCursors.map((cursor) => (
<Cursor
key={cursor.clientId}
coors={flowToScreenPosition({
x: cursor.cursor!.x,
y: cursor.cursor!.y,
})}
color={cursor.color}
/>
))}
</Panel>
);
}
14 changes: 13 additions & 1 deletion frontend/src/components/canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import { cn } from "@/lib/utils";
import { useQueryClient } from "@tanstack/react-query";
import useYDocStore from "@/store/useYDocStore";

import { useCollaborativeCursors } from "@/hooks/useCursor";
import { CollaborativeCursors } from "../CursorView";

const proOptions = { hideAttribution: true };

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

const { ydoc } = useYDocStore();

const { cursors, handleMouseMove, handleNodeDrag, handleMouseLeave } =
useCollaborativeCursors({
ydoc,
roomName: "flow-room",
});

const provider = useRef<WebsocketProvider>();
const existingPageIds = useRef(new Set<string>());

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

return (
<div className={cn("", className)}>
<div className={cn("", className)} onMouseMove={handleMouseMove}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={handleNodesChange}
onEdgesChange={handleEdgesChange}
onMouseLeave={handleMouseLeave}
onNodeDrag={handleNodeDrag}
onConnect={onConnect}
proOptions={proOptions}
nodeTypes={nodeTypes}
Expand All @@ -229,6 +240,7 @@ function Flow({ className }: CanvasProps) {
<Controls />
<MiniMap />
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
<CollaborativeCursors cursors={cursors} />
</ReactFlow>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/cursor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function Cursor({ coors, color = "#ffb8b9" }: CursorProps) {
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"
fill={color}
stroke="black"
stroke-width="4"
strokeWidth="4"
/>
</svg>
</motion.div>
Expand Down
115 changes: 115 additions & 0 deletions frontend/src/hooks/useCursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { useReactFlow, type XYPosition } from "@xyflow/react";
import * as Y from "yjs";
import { useCallback, useEffect, useRef, useState } from "react";
import { WebsocketProvider } from "y-websocket";

const CURSOR_COLORS = [
"#7d7b94",
"#41c76d",
"#f86e7e",
"#f6b8b8",
"#f7d353",
"#3b5bf7",
"#59cbf7",
] as const;

export interface AwarenessState {
cursor: XYPosition | null;
color: string;
clientId: number;
}

interface CollaborativeCursorsProps {
ydoc: Y.Doc;
roomName?: string;
}

export function useCollaborativeCursors({
ydoc,
roomName = "cursor-room",
}: CollaborativeCursorsProps) {
const flowInstance = useReactFlow();
const provider = useRef<WebsocketProvider>();
const [cursors, setCursors] = useState<Map<number, AwarenessState>>(
new Map(),
);
const clientId = useRef<number | null>(null);
const userColor = useRef(
CURSOR_COLORS[Math.floor(Math.random() * CURSOR_COLORS.length)],
);

useEffect(() => {
const wsProvider = new WebsocketProvider(
import.meta.env.VITE_WS_URL,
roomName,
ydoc,
);

provider.current = wsProvider;
clientId.current = wsProvider.awareness.clientID;

wsProvider.awareness.setLocalState({
cursor: null,
color: userColor.current,
clientId: wsProvider.awareness.clientID,
});

wsProvider.awareness.on("change", () => {
const states = new Map(
Array.from(
wsProvider.awareness.getStates() as Map<number, AwarenessState>,
).filter(
([key, state]) => key !== clientId.current && state.cursor !== null,
),
);
setCursors(states);
});

return () => {
wsProvider.destroy();
};
}, [ydoc, roomName]);

const updateCursorPosition = useCallback(
(x: number | null, y: number | null) => {
if (!provider.current?.awareness) return;

const cursor =
x !== null && y !== null
? flowInstance?.screenToFlowPosition({ x, y })
: null;

provider.current.awareness.setLocalState({
cursor,
color: userColor.current,
clientId: provider.current.awareness.clientID,
});
},
[flowInstance],
);

const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
updateCursorPosition(e.clientX, e.clientY);
},
[updateCursorPosition],
);

const handleNodeDrag = useCallback(
(e: React.MouseEvent) => {
updateCursorPosition(e.clientX, e.clientY);
},
[updateCursorPosition],
);

const handleMouseLeave = useCallback(() => {
updateCursorPosition(null, null);
}, [updateCursorPosition]);

return {
cursors,
handleMouseMove,
handleNodeDrag,
handleMouseLeave,
};
}

0 comments on commit 835f0bd

Please sign in to comment.