|
| 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