Skip to content

Commit

Permalink
feat: Adds ability to use custom ContainerID to persist prosemirror d…
Browse files Browse the repository at this point in the history
…ocument to. (#20)

And provide a `user` object for CurosrPlugin that renders the awareness cursor with a custom name and color

* Update storybook example and add BC example.

* feat: add ability to use ContainerID to persist updates to

* feat: adds user option on cursor plugin to allow for rendering custom name and color

* fix: use of commit's origin and message & reset format

* fix: broadcast channel example

---------

Co-authored-by: Zixuan Chen <[email protected]>
  • Loading branch information
raymonddaikon and zxch3n authored Oct 31, 2024
1 parent a83c2fa commit 224510f
Show file tree
Hide file tree
Showing 7 changed files with 5,209 additions and 4,133 deletions.
128 changes: 122 additions & 6 deletions examples/stories/src/stories/Editor.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { Meta } from "@storybook/react";

import { CursorAwareness, LoroDocType } from "loro-prosemirror";
import { useEffect, useRef } from "react";
import { Editor } from "./Editor";
import { LoroDoc } from "loro-crdt";
import { LoroDoc, VersionVector } from "loro-crdt";
import { useEffect, useRef, useState } from "react";
import { CursorAwareness, LoroDocType } from "loro-prosemirror";

const meta = {
title: "Editor/Basic",
Expand All @@ -27,6 +27,56 @@ export const Basic = () => {
);
};

type UpdateType = "ephemeral" | "awareness" | "crdt";
type UpdateMessage = {
type: "update";
updateType: UpdateType;
payload: Uint8Array;
};

function parseMessage(data: Uint8Array): UpdateMessage {
const messageType = data[0];
switch (messageType) {
case 1: {
// Update
const updateType = (() => {
switch (data[1]) {
case 0:
return "ephemeral";
case 1:
return "awareness";
case 2:
return "crdt";
default:
throw new Error(`Unknown update type: ${data[1]}`);
}
})();
return {
type: "update",
updateType,
payload: data.slice(2),
};
}
default:
throw new Error(`Unknown message type: ${messageType}`);
}
}

function encodeUpdateMessage(
updateType: UpdateType,
payload: Uint8Array,
): Uint8Array {
const message = new Uint8Array(2 + payload.length);
message[0] = 1;
message[1] = updateType === "ephemeral"
? 0
: updateType === "awareness"
? 1
: 2;
message.set(payload, 2);
return message;
}

export const Sync = () => {
const loroARef = useRef<LoroDocType>(new LoroDoc());
const idA = loroARef.current.peerIdStr;
Expand All @@ -38,14 +88,20 @@ export const Sync = () => {
loroARef.current.subscribe((event) => {
if (event.by === "local") {
loroBRef.current.import(
loroARef.current.exportFrom(loroBRef.current.oplogVersion()),
loroARef.current.export({
mode: "update",
from: loroBRef.current.oplogVersion(),
}),
);
}
});
loroBRef.current.subscribe((event) => {
if (event.by === "local") {
loroARef.current.import(
loroBRef.current.exportFrom(loroARef.current.oplogVersion()),
loroBRef.current.export({
mode: "update",
from: loroARef.current.oplogVersion(),
}),
);
}
});
Expand All @@ -63,10 +119,70 @@ export const Sync = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div>
<Editor
loro={loroARef.current}
awareness={awarenessA.current}
containerId={loroARef.current?.getMap("doc")?.id}
/>
<Editor
loro={loroBRef.current}
awareness={awarenessB.current}
containerId={loroARef.current?.getMap("doc")?.id}
/>
</div>
);
};

export const BroadcastChannelExample = () => {
const bcA = useRef<BroadcastChannel>(new BroadcastChannel(`A`));
const loroARef = useRef<LoroDocType>(new LoroDoc());
const idA = loroARef.current.peerIdStr;
const awarenessA = useRef<CursorAwareness>(new CursorAwareness(idA));
const [lastStateA, setLastStateA] = useState<VersionVector | undefined>();
useEffect(() => {
bcA.current.onmessage = (event) => {
const parsedMessage = parseMessage(event.data);
if (parsedMessage.type === "update") {
// Handle different update types
switch (parsedMessage.updateType) {
case "ephemeral":
break;
case "awareness":
break;
case "crdt":
loroARef.current.import(parsedMessage.payload);
loroARef.current.commit({ origin: "sys:bc-update" });
break;
}
}
};
loroARef.current.subscribe((event) => {
if (event.by === "local") {
bcA.current.postMessage(
encodeUpdateMessage(
"crdt",
loroARef.current.export({ mode: "update", from: lastStateA }),
),
);
setLastStateA(loroARef.current.version());
}
});
awarenessA.current.addListener((_state, origin) => {
if (origin === "local") {
bcA.current.postMessage(
encodeUpdateMessage("awareness", awarenessA.current.encode([idA])),
);
}
});

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div>
<Editor loro={loroARef.current} awareness={awarenessA.current} />
<Editor loro={loroBRef.current} awareness={awarenessB.current} />
</div>
);
};
22 changes: 14 additions & 8 deletions examples/stories/src/stories/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { useEffect, useRef } from "react";
import {
CursorAwareness,
LoroCursorPlugin,
LoroDocType,
LoroSyncPlugin,
LoroUndoPlugin,
undo,
redo,
LoroDocType,
undo,
} from "loro-prosemirror";
import "./Editor.css";
import { LoroDoc } from "loro-crdt";
import { ContainerID, LoroDoc } from "loro-crdt";
import { buildMenuItems } from "./menu";

const mySchema = new Schema({
Expand All @@ -27,16 +27,22 @@ const mySchema = new Schema({
const doc = DOMParser.fromSchema(mySchema).parse(document.createElement("div"));

/* eslint-disable */
const plugins = exampleSetup({ schema: mySchema, history: false, menuContent: buildMenuItems(mySchema).fullMenu as any });
const plugins = exampleSetup({
schema: mySchema,
history: false,
menuContent: buildMenuItems(mySchema).fullMenu as any,
});

export function Editor({
loro,
awareness,
onCreateLoro,
containerId,
}: {
loro?: LoroDocType;
awareness?: CursorAwareness;
onCreateLoro?: (loro: LoroDocType) => void;
containerId?: ContainerID;
}) {
const editorRef = useRef<null | EditorView>(null);
const editorDom = useRef(null);
Expand All @@ -56,12 +62,12 @@ export function Editor({

const all = [
...plugins,
LoroSyncPlugin({ doc: loroRef.current! }),
LoroSyncPlugin({ doc: loroRef.current!, containerId }),
LoroUndoPlugin({ doc: loroRef.current! }),
keymap({
"Mod-z": state => undo(state, () => {}),
"Mod-y": state => redo(state, () => {}),
"Mod-Shift-z": state => redo(state, () => {}),
"Mod-z": (state) => undo(state, () => {}),
"Mod-y": (state) => redo(state, () => {}),
"Mod-Shift-z": (state) => redo(state, () => {}),
}),
];
if (awareness) {
Expand Down
Loading

0 comments on commit 224510f

Please sign in to comment.