4.0 KB133 lines
Blame
1"use client";
2
3import { useEffect, useRef, useState, useCallback } from "react";
4import { io, type Socket } from "socket.io-client";
5import { useAuth } from "@/lib/auth";
6
7export interface CollabUser {
8 id: string;
9 name: string;
10 color: string;
11 cursor: { x: number; y: number } | null;
12 activeTab: string | null;
13}
14
15export interface CollabNote {
16 id: string;
17 diagramId: string;
18 diagramTitle?: string;
19 author: string;
20 text: string;
21 x?: number;
22 y?: number;
23 targetNode?: string | null;
24 timestamp: string;
25 editedAt?: string;
26}
27
28interface UseCollabSocketOptions {
29 owner: string;
30 repo: string;
31 enabled?: boolean;
32}
33
34export function useCollabSocket({ owner, repo, enabled = true }: UseCollabSocketOptions) {
35 const { token } = useAuth();
36 const socketRef = useRef<Socket | null>(null);
37 const [connected, setConnected] = useState(false);
38 const [users, setUsers] = useState<Record<string, CollabUser>>({});
39 const [notes, setNotes] = useState<Record<string, CollabNote[]>>({});
40
41 // Event callbacks — set by consumers
42 const onCursorUpdatedRef = useRef<((data: any) => void) | null>(null);
43 const onCursorRemovedRef = useRef<((data: any) => void) | null>(null);
44 const onDiagramCodeRef = useRef<((data: any) => void) | null>(null);
45 const onYjsSyncRef = useRef<((data: any) => void) | null>(null);
46 const onYjsUpdateRef = useRef<((data: any) => void) | null>(null);
47
48 useEffect(() => {
49 if (!enabled || !token || !owner || !repo) return;
50
51 // Socket.IO server runs as a co-located process. In production,
52 // Caddy routes /socket.io/* to the socket port on the same origin.
53 // In dev, use NEXT_PUBLIC_COLLAB_SOCKET_URL (e.g. http://localhost:3334).
54 const collabUrl =
55 process.env.NEXT_PUBLIC_COLLAB_SOCKET_URL || window.location.origin;
56
57 const socket = io(`${collabUrl}/collab`, {
58 auth: { token },
59 transports: ["polling", "websocket"],
60 });
61
62 socketRef.current = socket;
63
64 socket.on("connect", () => {
65 setConnected(true);
66 socket.emit("join-room", { owner, repo });
67 });
68
69 socket.on("disconnect", () => setConnected(false));
70
71 socket.on("room-state", (data: { notes: Record<string, CollabNote[]>; users: Record<string, CollabUser> }) => {
72 setNotes(data.notes);
73 setUsers(data.users);
74 });
75
76 socket.on("users-updated", (data: Record<string, CollabUser>) => {
77 setUsers(data);
78 });
79
80 socket.on("note-added", (note: CollabNote) => {
81 setNotes((prev) => ({
82 ...prev,
83 [note.diagramId]: [...(prev[note.diagramId] ?? []), note],
84 }));
85 });
86
87 socket.on("note-edited", ({ noteId, diagramId, text, editedAt }: { noteId: string; diagramId: string; text: string; editedAt: string }) => {
88 setNotes((prev) => ({
89 ...prev,
90 [diagramId]: (prev[diagramId] ?? []).map((n) =>
91 n.id === noteId ? { ...n, text, editedAt } : n
92 ),
93 }));
94 });
95
96 socket.on("note-deleted", ({ noteId, diagramId }: { noteId: string; diagramId: string }) => {
97 setNotes((prev) => ({
98 ...prev,
99 [diagramId]: (prev[diagramId] ?? []).filter((n) => n.id !== noteId),
100 }));
101 });
102
103 socket.on("cursor-updated", (data: any) => onCursorUpdatedRef.current?.(data));
104 socket.on("cursor-removed", (data: any) => onCursorRemovedRef.current?.(data));
105 socket.on("diagram-code", (data: any) => onDiagramCodeRef.current?.(data));
106 socket.on("yjs-sync", (data: any) => onYjsSyncRef.current?.(data));
107 socket.on("yjs-update", (data: any) => onYjsUpdateRef.current?.(data));
108
109 return () => {
110 socket.disconnect();
111 socketRef.current = null;
112 setConnected(false);
113 };
114 }, [enabled, token, owner, repo]);
115
116 const emit = useCallback((event: string, data?: any) => {
117 socketRef.current?.emit(event, data);
118 }, []);
119
120 return {
121 socket: socketRef,
122 connected,
123 users,
124 notes,
125 emit,
126 onCursorUpdated: onCursorUpdatedRef,
127 onCursorRemoved: onCursorRemovedRef,
128 onDiagramCode: onDiagramCodeRef,
129 onYjsSync: onYjsSyncRef,
130 onYjsUpdate: onYjsUpdateRef,
131 };
132}
133