Reputation: 1
I'm facing an issue with my collaborative text editor built using Yjs, y-websocket, and Quill. The editor allows users to work on notes collaboratively in real time. However, I'm encountering text duplication under the following conditions:
Issue Description
Code Implementation
Here is the relevant implementation for the editor (TextEditor.jsx) and the Yjs WebSocket server (yjs-server.cjs).
TextEditor.jsx
import React, { useEffect, useRef, useState } from "react";
import Quill from "quill";
import "quill/dist/quill.snow.css";
import "./TextEditor.css";
import { useParams, useNavigate } from "react-router-dom";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { QuillBinding } from "y-quill";
import { useAuth0 } from "@auth0/auth0-react";
import QuillCursors from "quill-cursors";
Quill.register("modules/cursors", QuillCursors);
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
const YJS_URL = import.meta.env.VITE_YJS_URL || "ws://localhost:1234";
const SAVE_INTERVAL_MS = 2000;
const TOOLBAR_OPTIONS = [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ font: [] }],
[{ list: "ordered" }, { list: "bullet" }],
["bold", "italic", "underline"],
[{ color: [] }, { background: [] }],
[{ script: "sub" }, { script: "super" }],
[{ align: [] }],
["image", "blockquote", "code-block"],
["clean"],
];
export default function TextEditor() {
const { id: documentId } = useParams();
const navigate = useNavigate();
const editorRef = useRef();
const quillRef = useRef();
const ydocRef = useRef();
const ytextRef = useRef();
const [noteName, setNoteName] = useState("Untitled Note");
const [isCollaborative, setIsCollaborative] = useState(false);
const [resourceId, setResourceId] = useState(null);
const { user } = useAuth0();
useEffect(() => {
const fetchInitialContent = async () => {
try {
const response = await fetch(`${BACKEND_URL}/get_note?note_id=${documentId}`);
const data = await response.json();
if (response.ok) {
const content = data.content ? JSON.parse(data.content) : null;
if (ytextRef.current) {
ytextRef.current.delete(0, ytextRef.current.length); // Clear existing content
}
if (content && ytextRef.current) {
ytextRef.current.applyDelta(content.ops); // Apply new content
}
setNoteName(data.name || "Untitled Note");
setResourceId(data.resource_id);
const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(
documentId
);
setIsCollaborative(isUUID);
} else {
if (data.error === "Unauthorized access") navigate("/");
}
} catch (error) {
console.error("Error fetching initial content:", error);
navigate("/");
}
};
fetchInitialContent();
}, [documentId, BACKEND_URL, navigate]);
useEffect(() => {
if (editorRef.current && !quillRef.current && resourceId) {
editorRef.current.innerHTML = "";
const ydoc = new Y.Doc();
ydocRef.current = ydoc;
const provider = new WebsocketProvider(YJS_URL, resourceId.toString(), ydoc);
const ytext = ydoc.getText("quill");
ytextRef.current = ytext;
const quill = new Quill(editorRef.current, {
theme: "snow",
modules: {
toolbar: TOOLBAR_OPTIONS,
cursors: true,
history: { userOnly: true },
},
});
quill.disable();
quillRef.current = quill;
const binding = new QuillBinding(ytext, quill, provider.awareness);
provider.on("status", (event) => {
if (event.status === "connected") quill.enable();
else quill.disable();
});
return () => {
binding.destroy();
provider.destroy();
ydoc.destroy();
quill.off();
quillRef.current = null;
};
}
}, [resourceId, YJS_URL]);
return (
<div>
<input
type="text"
value={noteName}
onChange={(e) => setNoteName(e.target.value)}
placeholder="Enter note title"
/>
<div ref={editorRef}></div>
</div>
);
}
yjs-server.cjs
const http = require("http");
const express = require("express");
const WebSocket = require("ws");
const { setupWSConnection } = require("y-websocket/utils");
const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
const port = process.env.PORT || 1234;
wss.on("connection", (ws, req) => {
const ip = req.socket.remoteAddress;
console.log(`New connection from ${ip}`);
setupWSConnection(ws, req);
});
server.listen(port, () => {
console.log(`Yjs WebSocket server running on port ${port}`);
});
Environment Details
What I've Tried
Expected Behavior
Thanks to anyone who can help me resolve this duplication issue.
Upvotes: 0
Views: 38