Skip to content

Commit 8f6973c

Browse files
Merge pull request #20 from SuperViz/feat/yjs-provider
feat: yjs provider
2 parents 9a3f9ab + 3b00d5f commit 8f6973c

56 files changed

Lines changed: 3585 additions & 423 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/checks.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,14 @@ jobs:
234234
env:
235235
NPM_CONFIG_USERCONFIG: .npmrc.ci
236236
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
237+
- name: Create a .version.js file
238+
run: |
239+
touch packages/realtime/.version.js && echo "echo \"export const version = 'test'\" > packages/realtime/.version.js" | bash -
240+
touch packages/sdk/.version.js && echo "echo \"export const version = 'test'\" > packages/sdk/.version.js" | bash -
241+
- name: Create a .remote-config.js file
242+
run: |
243+
touch packages/realtime/.remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com' }};\" > packages/realtime/.remote-config.js" | bash -
244+
touch packages/sdk/.remote-config.js && echo "echo \"module.exports = { remoteConfig: { apiUrl: 'https://dev.nodeapi.superviz.com', conferenceLayerUrl: 'https://video-frame.superviz.com/lab/index.html'}};\" > .remote-config.js" | bash -
237245
- name: Run tests
238246
run: pnpm run test:unit:ci --filter=@superviz/yjs
239247
- name: Post PR Comment

.github/workflows/yjs.ci.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: Yjs Provider - Publish Package
2+
on:
3+
push:
4+
branches:
5+
- lab
6+
- beta
7+
- main
8+
paths:
9+
- "packages/yjs/**"
10+
- ".github/workflows/yjs.ci.yml"
11+
jobs:
12+
package:
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
node-version: [20]
17+
steps:
18+
- uses: actions/checkout@v4
19+
- name: Install pnpm
20+
uses: pnpm/action-setup@v4
21+
with:
22+
version: 9.10.0
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: ${{ matrix.node-version }}
27+
cache: "pnpm"
28+
- name: Install dependencies
29+
run: pnpm install --no-frozen-lockfile
30+
env:
31+
NPM_CONFIG_USERCONFIG: .npmrc.ci
32+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
33+
- run: git config --global user.name SuperViz
34+
- run: git config --global user.email ci@superviz.com
35+
- name: Publish npm package
36+
run: npm whoami && pnpm run semantic-release --filter=@superviz/yjs
37+
env:
38+
NPM_CONFIG_USERCONFIG: .npmrc.ci
39+
GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }}
40+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
41+
slack:
42+
needs: package
43+
name: Slack Notification
44+
runs-on: ubuntu-latest
45+
steps:
46+
- uses: actions/checkout@v2
47+
- name: Slack Notification
48+
uses: rtCamp/action-slack-notify@v2
49+
env:
50+
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
51+
SLACK_ICON: https://avatars.slack-edge.com/2020-11-18/1496892993975_af721d1c045bea2d5a46_48.png
52+
MSG_MINIMAL: true
53+
SLACK_USERNAME: Deploy yjs provider version ${{ github.ref_name }}

apps/playground/package.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,30 @@
1010
"preview": "vite preview"
1111
},
1212
"dependencies": {
13+
"@monaco-editor/react": "^4.6.0",
1314
"@superviz/autodesk-viewer-plugin": "workspace:*",
1415
"@superviz/matterport-plugin": "workspace:*",
1516
"@superviz/realtime": "workspace:*",
1617
"@superviz/sdk": "workspace:*",
1718
"@superviz/threejs-plugin": "workspace:*",
1819
"@superviz/yjs": "workspace:*",
1920
"@types/three": "^0.167.1",
21+
"lib0": "^0.2.97",
2022
"lodash": "^4.17.21",
23+
"monaco-editor": "^0.51.0",
24+
"quill-cursors": "^4.0.3",
2125
"react": "^18.3.1",
2226
"react-dom": "^18.3.1",
2327
"react-helmet-async": "^2.0.5",
28+
"react-quill-new": "^3.3.2",
2429
"react-router-dom": "^6.26.1",
2530
"three": "0.167.1",
2631
"three-mesh-bvh": "^0.7.8",
2732
"uuid": "^10.0.0",
33+
"y-monaco": "^0.1.6",
34+
"y-protocols": "^1.0.6",
35+
"y-quill": "^1.0.0",
36+
"yjs": "^13.3.1",
2837
"zod": "^3.23.8"
2938
},
3039
"devDependencies": {

apps/playground/src/lib/sdk/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ export {
88
MousePointers,
99
ParticipantType,
1010
type LauncherFacade,
11+
type Participant,
1112
} from "@superviz/sdk";
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { v4 as generateId } from "uuid";
3+
4+
import { getConfig } from "../config";
5+
6+
import * as Y from "yjs";
7+
import { SuperVizYjsProvider } from "@superviz/yjs";
8+
9+
import { MonacoBinding } from "y-monaco";
10+
import "../styles/yjs.css";
11+
import {
12+
Room,
13+
type LauncherFacade,
14+
type Participant,
15+
WhoIsOnline,
16+
} from "../lib/sdk";
17+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
18+
import Editor from "@monaco-editor/react";
19+
20+
const SUPERVIZ_KEY = getConfig<string>("keys.superviz");
21+
const SUPERVIZ_ROOM_PREFIX = getConfig<string>("roomPrefix");
22+
23+
const componentName = "yjs-monaco-wio";
24+
25+
function setStyles(
26+
states: Map<number, Record<string, any>>,
27+
ids: Set<number>
28+
): number[] {
29+
const stylesheet = document.getElementById("sv-yjs-monaco");
30+
let styles = "";
31+
32+
const idsList = [];
33+
for (const [id, state] of states) {
34+
if (ids.has(id) || !state.participant) continue;
35+
idsList.push(id);
36+
37+
styles += `
38+
.yRemoteSelection-${id},
39+
.yRemoteSelectionHead-${id} {
40+
--presence-color: ${state.participant.slot.color};
41+
}
42+
43+
.yRemoteSelectionHead-${id}:after {
44+
content: "${state.participant.name}";
45+
--sv-text-color: ${state.participant.slot.textColor};
46+
}
47+
`;
48+
}
49+
50+
stylesheet!.innerText = styles;
51+
52+
return idsList;
53+
}
54+
55+
export function YjsMonacoWio() {
56+
const ydoc = useMemo(() => new Y.Doc(), []);
57+
58+
const [editor, setEditor] = useState<any>(null);
59+
const [localParticipant, setLocalParticipant] =
60+
useState<Partial<Participant>>();
61+
const [ids, setIds] = useState(new Set<number>());
62+
const [joinedRoom, setJoinedRoom] = useState(false);
63+
64+
const room = useRef<LauncherFacade>();
65+
const provider = useMemo<SuperVizYjsProvider>(
66+
() => new SuperVizYjsProvider(ydoc),
67+
[ydoc]
68+
);
69+
const wio = useRef<WhoIsOnline>();
70+
const loaded = useRef(false);
71+
72+
const initializeSuperViz = useCallback(async () => {
73+
if (loaded.current) return;
74+
loaded.current = true;
75+
76+
const uuid = generateId();
77+
78+
room.current = await Room(SUPERVIZ_KEY, {
79+
roomId: `${SUPERVIZ_ROOM_PREFIX}-${componentName}`,
80+
participant: {
81+
name: "Participant",
82+
id: uuid,
83+
},
84+
group: {
85+
name: SUPERVIZ_ROOM_PREFIX,
86+
id: SUPERVIZ_ROOM_PREFIX,
87+
},
88+
environment: "dev",
89+
debug: true,
90+
});
91+
92+
wio.current = new WhoIsOnline();
93+
94+
room.current.subscribe("participant.updated", (data) => {
95+
if (!data.slot?.index) return;
96+
97+
provider.awareness?.setLocalStateField("participant", {
98+
id: data.id,
99+
slot: data.slot,
100+
name: data.name,
101+
});
102+
103+
setLocalParticipant({
104+
id: data.id,
105+
slot: data.slot,
106+
name: data.name,
107+
});
108+
});
109+
110+
const style = document.createElement("style");
111+
style.id = "sv-yjs-monaco";
112+
document.head.appendChild(style);
113+
}, [provider.awareness]);
114+
115+
useEffect(() => {
116+
initializeSuperViz();
117+
118+
return () => {
119+
room.current?.removeComponent(wio.current);
120+
room.current?.removeComponent(provider);
121+
room.current?.destroy();
122+
};
123+
}, []);
124+
125+
const joinRoom = useCallback(() => {
126+
if (joinedRoom || !room.current) return;
127+
setJoinedRoom(true);
128+
129+
if (localParticipant) {
130+
provider.awareness?.setLocalStateField("participant", localParticipant);
131+
}
132+
133+
const updateStyles = () => {
134+
const states = provider.awareness?.getStates();
135+
const idsList = setStyles(states, ids);
136+
137+
setIds(new Set(idsList));
138+
};
139+
140+
provider.on("connect", updateStyles);
141+
provider.awareness?.on("update", updateStyles);
142+
143+
room.current.addComponent(provider);
144+
room.current.addComponent(wio.current);
145+
}, [room, joinedRoom, setIds, ids, localParticipant, provider]);
146+
147+
const leaveRoom = useCallback(() => {
148+
if (!joinedRoom || !room.current) return;
149+
setJoinedRoom(false);
150+
151+
setIds(new Set());
152+
room.current.removeComponent(wio.current);
153+
room.current.removeComponent(provider);
154+
}, [room, joinedRoom, provider]);
155+
156+
useEffect(() => {
157+
if (!provider || editor == null) return;
158+
159+
const binding = new MonacoBinding(
160+
ydoc.getText("monaco"),
161+
editor.getModel()!,
162+
new Set([editor]),
163+
provider.awareness
164+
);
165+
return () => {
166+
binding.destroy();
167+
};
168+
}, [ydoc, provider, editor]);
169+
170+
return (
171+
<div className="p-5 h-full bg-gray-200 flex flex-col gap-5">
172+
<div className="flex items-center w-full justify-center gap-10">
173+
<button
174+
onClick={joinRoom}
175+
disabled={joinedRoom || !room}
176+
className="bg-sv-purple text-white h-10 px-4 rounded-md hover:bg-sv-primary-900 transition-all duration-300 disabled:bg-sv-primary-200 disabled:cursor-not-allowed"
177+
>
178+
Join room
179+
</button>
180+
<button
181+
onClick={leaveRoom}
182+
disabled={!joinedRoom || !room}
183+
className="text-sv-gray-400 border-sv-gray-400 border h-10 px-4 rounded-md hover:bg-sv-gray-400 hover:text-white transition-all duration-300 cursor-pointer disabled:bg-sv-gray-100 disabled:cursor-not-allowed disabled:text-sv-gray-400"
184+
>
185+
Leave room
186+
</button>
187+
</div>
188+
<div className="bg-[#1e1e1e] shadow-none h-[90%] overflow-auto rounded-sm">
189+
<div className="yRemoteSelectionHead"></div>
190+
<Editor
191+
defaultValue="// Connect to the room to start collaborating"
192+
defaultLanguage="typescript"
193+
onMount={(editor) => {
194+
setEditor(editor);
195+
}}
196+
options={{
197+
padding: {
198+
top: 32,
199+
},
200+
}}
201+
theme="vs-dark"
202+
/>
203+
</div>
204+
</div>
205+
);
206+
}

0 commit comments

Comments
 (0)