diff --git a/api/index.js b/api/index.js index 6588832..4303364 100644 --- a/api/index.js +++ b/api/index.js @@ -27,26 +27,40 @@ app.get('/test', (res) => { }) io.on('connection', (socket) => { - console.log('user connected socket') + + socket.on('joinRoom', ({room_id}) => { + socket.join(room_id); + console.log(`User ${socket.id} joined room ${room_id}`); + }) + socket.on('draw', (data)=>{ - socket.broadcast.emit('draw', data); + const room = data.room_id; + socket.to(room).emit('draw', data); }) - socket.on('clear', () => { - io.emit('clear'); + socket.on('clear', (data) => { + const room = data.room_id; + socket.to(room).emit('clear'); }) socket.on('open-text-editor', data => { - socket.broadcast.emit("open-text-editor", data); + const room = data.room_id; + socket.to(room).emit("open-text-editor", data); }) socket.on('close-text-editor', data => { - socket.broadcast.emit("close-text-editor", data); + const room = data.room_id; + socket.to(room).emit("close-text-editor", data); }) socket.on("text-updated", (data) => { - socket.broadcast.emit("text-updated", data); + const room = data.room_id; + socket.to(room).emit("text-updated", data); }); + + socket.on("disconnect", () => { + console.log(`${socket.id} disconnected`); + }) }) server.listen(PORT, ()=>{ diff --git a/client/index.html b/client/index.html index 81684c8..c6dc82f 100644 --- a/client/index.html +++ b/client/index.html @@ -9,6 +9,5 @@
- diff --git a/client/package-lock.json b/client/package-lock.json index 345663b..555bdb0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -14,6 +14,7 @@ "react-icons": "^5.2.0", "recoil": "^0.7.7", "socket.io-client": "^4.7.4", + "uuid": "^10.0.0", "y-socket.io": "^1.1.3", "yjs": "^13.6.15" }, @@ -5658,6 +5659,19 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/client/package.json b/client/package.json index 7d308df..65c573d 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ "react-icons": "^5.2.0", "recoil": "^0.7.7", "socket.io-client": "^4.7.4", + "uuid": "^10.0.0", "y-socket.io": "^1.1.3", "yjs": "^13.6.15" }, diff --git a/client/src/App.jsx b/client/src/App.jsx index 1ff1723..0ef4090 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,14 +1,14 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useRef, useState } from "react"; +import { io } from "socket.io-client"; import "./App.css"; -import socket from "./socket"; - +import InfoMsg from "./components/InfoMsg"; import Sidebar from "./components/Sidebar"; import Canvas from "./components/Canvas"; import Menu from "./components/Menu"; import EraserCursor from "./components/EraserCursor"; import TextEditor from "./components/TextEditor"; -import { useRecoilValue, useRecoilState } from "recoil"; +import { useRecoilValue, useRecoilState, useSetRecoilState } from "recoil"; import { eraserState, cursorPosition, @@ -16,9 +16,14 @@ import { canvasState, showMenuState, showTextEditor, + collaborationStarted, + showMsg, + roomIdAtom, + messageTxtAtom } from "./atoms"; +import { useSocket } from "./Context"; -socket.connect(); +const PORT = "http://localhost:8000"; function App() { const [showMenu, setShowMenu] = useRecoilState(showMenuState); @@ -32,7 +37,13 @@ function App() { const canvasColor = useRecoilValue(canvasColors); const [currentCanvas, setCanvas] = useRecoilState(canvasState); const textEditor = useRecoilValue(showTextEditor); - + const setTextEditor = useSetRecoilState(showTextEditor); + const hasCollaborationStarted = useRecoilValue(collaborationStarted); + const setCollaborationFlag = useSetRecoilState(collaborationStarted); + const [showMessage, setShowMsg] = useRecoilState(showMsg); + const [roomId, setRoomId] = useRecoilState(roomIdAtom); + const { socket, setSocket } = useSocket(); + const [ messageText, setMessageText ] = useRecoilState(messageTxtAtom); function toggleMenu() { setShowMenu(!showMenu); @@ -76,7 +87,9 @@ function App() { const endX = e.clientX - canvas.getBoundingClientRect().left; const endY = e.clientY - canvas.getBoundingClientRect().top; drawLine(startX, startY, endX, endY, penColor); - socket.emit("draw", { startX, startY, endX, endY, penColor, lineWidth }); + if (hasCollaborationStarted && socket) { + socket.emit("draw", { startX, startY, endX, endY, penColor, lineWidth, room_id: roomId }); + } setStartX(endX); setStartY(endY); } @@ -104,29 +117,31 @@ function App() { canvas.removeEventListener("mousedown", handleMousedown); canvas.removeEventListener("mouseup", handleMouseup); }; - }, [penColor, eraserMode, position, ctx, isDrawing, startX, startY]); + }, [socket, penColor, eraserMode, position, ctx, isDrawing, startX, startY]); useEffect(() => { - socket.on("draw", (data) => { - drawLine( - data.startX, - data.startY, - data.endX, - data.endY, - data.penColor, - data.lineWidth - ); - }); - - socket.on("clear", () => { - clearRect(); - }); - - return () => { - socket.off("draw"); - socket.off("clear"); - }; - }, [socket, ctx]); + if (hasCollaborationStarted && socket) { + socket.on("draw", (data) => { + drawLine( + data.startX, + data.startY, + data.endX, + data.endY, + data.penColor, + data.lineWidth + ); + }); + + socket.on("clear", () => { + clearRect(); + }); + + return () => { + socket.off("draw"); + socket.off("clear"); + }; + } + }, [socket, ctx, hasCollaborationStarted]); function clearRect() { if (ctx) { @@ -136,7 +151,10 @@ function App() { function clearOnClick() { clearRect(); - socket.emit("clear"); + if (hasCollaborationStarted && socket) { + const data = {room_id: roomId}; + socket.emit("clear", data); + } } function addStroke(e) { @@ -158,6 +176,71 @@ function App() { } } + const closeMsg = () => { + setMessageText(null); + setShowMsg(false); + } + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const roomID = urlParams.get("roomID"); + const RETRIES = 5; + const DELAY_DURATION = 2000; + let attempts = 0; + + const closeConnection = (socket) => { + if (attempts >= RETRIES) { + socket.disconnect(); + setCollaborationFlag(false); + console.log("Max socket calls exceeded. Please try connecting again.") + } + } + + if (roomID) { + const collaborationLink = window.location.href; + setRoomId(roomID); + setCollaborationFlag(true); + const newSocket = io(PORT); + + try { + newSocket.on("connect", () => { + console.log("connected"); + try { + newSocket.emit("joinRoom", { room_id: roomID }); + console.log(`joined room ${roomID}`); + setSocket(newSocket); + setMessageText(`Collaboration Link : ${collaborationLink}`); + setShowMsg(true); + } catch (error) { + console.log("Can't join the room", error); + } + }); + + newSocket.on("connect_error", (error) => { + attempts++; + console.log("Failed to connect to the socket server. Retrying...") + setTimeout(() => closeConnection(newSocket), DELAY_DURATION); + }) + + } catch (error) { + console.log("Can't connect", error); + } + } + }, []); + + // Hook to listen the events emitted by the server + useEffect(() => { + if (hasCollaborationStarted && socket) { + socket.on("open-text-editor", () => { + setTextEditor(true); + }); + + return () => { + socket.off("open-text-editor"); + }; + } + }, [showTextEditor, socket, hasCollaborationStarted]); + return (
} {showMenu && } {textEditor && } + {showMessage && }
); } diff --git a/client/src/Context.jsx b/client/src/Context.jsx new file mode 100644 index 0000000..4e0b97f --- /dev/null +++ b/client/src/Context.jsx @@ -0,0 +1,20 @@ +import React, { createContext, useContext, useState } from 'react'; + +// Create a Context for the socket +const SocketContext = createContext(null); + +// Create a custom hook to use the SocketContext +export const useSocket = () => { + return useContext(SocketContext); +}; + +// Create a provider component +export const SocketProvider = ({ children }) => { + const [socket, setSocket] = useState(null); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/client/src/atoms.js b/client/src/atoms.js index b7e9705..5b03d67 100644 --- a/client/src/atoms.js +++ b/client/src/atoms.js @@ -34,4 +34,24 @@ export const showTextEditor = atom({ export const textEditorInput = atom({ key: "textEditorInput", default: '' -}) \ No newline at end of file +}) + +export const collaborationStarted = atom({ + key: "collaboraionstarted", + default: false +}) + +export const showMsg = atom({ + key: "showMsg", + default: false +}) + +export const roomIdAtom = atom({ + key: "roomIdAtom", + default: null +}) + +export const messageTxtAtom = atom({ + key: "msgTxtAtom", + default: null +}); \ No newline at end of file diff --git a/client/src/components/InfoMsg.jsx b/client/src/components/InfoMsg.jsx new file mode 100644 index 0000000..7f1cbc4 --- /dev/null +++ b/client/src/components/InfoMsg.jsx @@ -0,0 +1,10 @@ +const InfoMsg = (props) => { + return ( +
+
×
+
{props.message}
+
+ ) +} + +export default InfoMsg; \ No newline at end of file diff --git a/client/src/components/Menu.jsx b/client/src/components/Menu.jsx index 016357a..b19b3be 100644 --- a/client/src/components/Menu.jsx +++ b/client/src/components/Menu.jsx @@ -3,18 +3,25 @@ import MenuItem from "./MenuItem"; import Socials from "./Socials"; import SponsorBtn from "./SponsorBtn"; import { useRecoilValue, useRecoilState, useSetRecoilState} from "recoil"; -import { canvasState, canvasColors, showTextEditor, showMenuState } from "../atoms"; +import { canvasState, canvasColors, showTextEditor, showMenuState, collaborationStarted, showMsg, roomIdAtom } from "../atoms"; import { jsPDF } from "jspdf"; -import socket from "../socket"; import { useCallback, useEffect, useRef } from "react"; +import InfoMsg from "./InfoMsg"; +import { useSocket } from "../Context"; +import redirectToCollabLink from "../generateLink"; function Menu(){ const canvas = useRecoilValue(canvasState); const canvasColor = useRecoilValue(canvasColors); const [textEditor, setTextEditor] = useRecoilState(showTextEditor) const setMenuStateFalse = useSetRecoilState(showMenuState); + const [hasCollaborationStarted, setCollaborationFlag] = useRecoilState(collaborationStarted); let isRendering = useRef(false); - + const showMessage = useRecoilValue(showMsg); + const changeShowMsg = useSetRecoilState(showMsg); + const roomId = useRecoilValue(roomIdAtom); + const { socket } = useSocket(); + // function to save canvas as pdf function saveAsPdf() { const canvasDataURL = canvas.toDataURL("image/png"); @@ -54,34 +61,35 @@ function Menu(){ } const openTextEditor = useCallback(() => { - const data = "ajeet"; - if(!isRendering.current){ + if(!isRendering.current && hasCollaborationStarted && socket){ + const data = {room_id: roomId}; socket.emit("open-text-editor", data); } setTextEditor(true); setMenuStateFalse(false); - }, [setMenuStateFalse, setTextEditor]) - - // Memoize the handler function to keep it stable and pass the ref - const handleOpenTextEditor = useCallback((rendering) => { - isRendering.current = rendering; - openTextEditor(); - }, [openTextEditor]) - - useEffect(() => { - socket.on("open-text-editor", (data) => { - handleOpenTextEditor(true); - }); + }, [setMenuStateFalse, setTextEditor, hasCollaborationStarted, socket]) - return () => { - socket.off("open-text-editor", handleOpenTextEditor); - }; - }, [handleOpenTextEditor]); + + const startCollab = () => { + setCollaborationFlag(true); + setMenuStateFalse(false); + redirectToCollabLink(); + }; + + const stopCollab = () => { + setCollaborationFlag(false); + setMenuStateFalse(false); + socket.disconnect(); + console.log("Disconnected from the collaboration room"); + } return (
- - + {hasCollaborationStarted ? ( + + ) : ( + + )} diff --git a/client/src/components/Socials.jsx b/client/src/components/Socials.jsx index f249415..392ed57 100644 --- a/client/src/components/Socials.jsx +++ b/client/src/components/Socials.jsx @@ -9,8 +9,8 @@ function Socials(){

Built By{" "} - - AJEET + + Ajeet

{ setTextEditorFalse(false); - if (!isRendering.current) { - socket.emit("close-text-editor"); + if (!isRendering.current && hasCollaborationStarted && socket) { + const data = {room_id: roomId}; + socket.emit("close-text-editor", data); } - }, [setTextEditorFalse]) + }, [setTextEditorFalse, hasCollaborationStarted, socket]) const handleRemoveTextEditor = useCallback((rendering) => { isRendering.current = rendering; @@ -26,23 +29,28 @@ function TextEditor() { }, [setInput]); useEffect(() => { - socket.on("close-text-editor", () => { - handleRemoveTextEditor(true); - }); - - socket.on("text-updated", data => { - handleTextEditorUpdate(data); - }); - - return () => { - socket.off("close-text-editor", handleRemoveTextEditor); - }; - }, [handleRemoveTextEditor, handleTextEditorUpdate]); + if (hasCollaborationStarted && socket) { + socket.on("close-text-editor", () => { + handleRemoveTextEditor(true); + }); + + socket.on("text-updated", data => { + handleTextEditorUpdate(data.value); + }); + + return () => { + socket.off("close-text-editor", handleRemoveTextEditor); + }; + } + }, [handleRemoveTextEditor, handleTextEditorUpdate, hasCollaborationStarted, socket]); function handleChange(event) { const value = event.target.value; - setInput(value); // this is asynchronous - socket.emit("text-updated", value); + setInput(value); + if (hasCollaborationStarted && socket) { + const data = {value, room_id: roomId} + socket.emit("text-updated", data); + } } return ( diff --git a/client/src/generateLink.js b/client/src/generateLink.js new file mode 100644 index 0000000..5b8312d --- /dev/null +++ b/client/src/generateLink.js @@ -0,0 +1,16 @@ +import {v4 as uuidv4} from "uuid"; + +const url = "http://localhost:5173/"; + +const generateCollabLink = () => { + const uniqueId = uuidv4(); + const link = `${url}?roomID=${uniqueId}`; + return link; +} + +const redirectToCollabLink = () => { + const link = generateCollabLink(); + window.location.replace(link); +}; + +export default redirectToCollabLink; \ No newline at end of file diff --git a/client/src/main.jsx b/client/src/main.jsx index e46b492..656d286 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -3,11 +3,14 @@ import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' import { RecoilRoot } from "recoil" +import { SocketProvider } from './Context.jsx' ReactDOM.createRoot(document.getElementById("root")).render( + + ); diff --git a/client/src/socket.js b/client/src/socket.js deleted file mode 100644 index f4b3145..0000000 --- a/client/src/socket.js +++ /dev/null @@ -1,11 +0,0 @@ -import { io } from "socket.io-client"; - -const PORT = "http://localhost:8000"; - -const socket = io(PORT); - -socket.on("connect", () => { - console.log("connected"); -}); - -export default socket;