diff --git a/examples/07-collaboration/06-ghost-writer/.bnexample.json b/examples/07-collaboration/06-ghost-writer/.bnexample.json new file mode 100644 index 0000000000..2d28ad5ec5 --- /dev/null +++ b/examples/07-collaboration/06-ghost-writer/.bnexample.json @@ -0,0 +1,10 @@ +{ + "playground": true, + "docs": false, + "author": "nperez0111", + "tags": ["Advanced", "Development", "Collaboration"], + "dependencies": { + "y-partykit": "^0.0.25", + "yjs": "^13.6.15" + } +} diff --git a/examples/07-collaboration/06-ghost-writer/App.tsx b/examples/07-collaboration/06-ghost-writer/App.tsx new file mode 100644 index 0000000000..4344c5c11a --- /dev/null +++ b/examples/07-collaboration/06-ghost-writer/App.tsx @@ -0,0 +1,126 @@ +import "@blocknote/core/fonts/inter.css"; +import "@blocknote/mantine/style.css"; +import { BlockNoteView } from "@blocknote/mantine"; +import { useCreateBlockNote } from "@blocknote/react"; + +import YPartyKitProvider from "y-partykit/provider"; +import * as Y from "yjs"; +import "./styles.css"; +import { useEffect, useState } from "react"; +// eslint-disable-next-line import/no-extraneous-dependencies +import { EditorView } from "prosemirror-view"; + +const params = new URLSearchParams(window.location.search); +const ghostWritingRoom = params.get("room"); +const ghostWriterIndex = parseInt(params.get("index") || "1"); +const isGhostWriting = Boolean(ghostWritingRoom); +const roomName = ghostWritingRoom || `ghost-writer-${Date.now()}`; +// Sets up Yjs document and PartyKit Yjs provider. +const doc = new Y.Doc(); +const provider = new YPartyKitProvider( + "blocknote-dev.yousefed.partykit.dev", + // Use a unique name as a "room" for your application. + roomName, + doc, +); + +/** + * Y-prosemirror has an optimization, where it doesn't send awareness updates unless the editor is currently focused. + * So, for the ghost writers, we override the hasFocus method to always return true. + */ +if (isGhostWriting) { + EditorView.prototype.hasFocus = () => true; +} + +const ghostContent = + "This demo shows a two-way sync of documents. It allows you to test collaboration features, and see how stable the editor is. "; + +export default function App() { + const [numGhostWriters, setNumGhostWriters] = useState(1); + const [isPaused, setIsPaused] = useState(false); + const editor = useCreateBlockNote({ + collaboration: { + // The Yjs Provider responsible for transporting updates: + provider, + // Where to store BlockNote data in the Y.Doc: + fragment: doc.getXmlFragment("document-store"), + // Information (name and color) for this user: + user: { + name: isGhostWriting + ? `Ghost Writer #${ghostWriterIndex}` + : "My Username", + color: isGhostWriting ? "#CCCCCC" : "#00ff00", + }, + }, + }); + + useEffect(() => { + if (!isGhostWriting || isPaused) { + return; + } + let index = 0; + let timeout: NodeJS.Timeout; + + const scheduleNextChar = () => { + const jitter = Math.random() * 200; // Random delay between 0-200ms + timeout = setTimeout(() => { + const firstBlock = editor.document?.[0]; + if (firstBlock) { + editor.insertInlineContent(ghostContent[index], { + updateSelection: true, + }); + index = (index + 1) % ghostContent.length; + } + scheduleNextChar(); + }, 50 + jitter); + }; + + scheduleNextChar(); + + return () => clearTimeout(timeout); + }, [editor, isPaused]); + + // Renders the editor instance. + return ( + <> + {isGhostWriting ? ( + + ) : ( + <> + + + + + )} + + + {!isGhostWriting && ( +
+ {Array.from({ length: numGhostWriters }).map((_, index) => ( +