From a18bcf9c147b162b83e0319905596949d4fd0a76 Mon Sep 17 00:00:00 2001 From: Ishaan Dey Date: Fri, 3 May 2024 14:27:45 -0700 Subject: [PATCH] mostly done liveblocks integration --- frontend/app/(app)/code/[id]/page.tsx | 14 ++-- frontend/app/api/lb-auth/route.ts | 1 - frontend/components/editor/index.tsx | 69 +++++++++++-------- frontend/components/editor/live/avatars.tsx | 21 ++++++ frontend/components/editor/live/cursors.tsx | 57 +++++++++++++++ .../components/editor/{ => live}/room.tsx | 13 ++-- frontend/liveblocks.config.ts | 21 +++--- 7 files changed, 147 insertions(+), 49 deletions(-) create mode 100644 frontend/components/editor/live/avatars.tsx create mode 100644 frontend/components/editor/live/cursors.tsx rename frontend/components/editor/{ => live}/room.tsx (61%) diff --git a/frontend/app/(app)/code/[id]/page.tsx b/frontend/app/(app)/code/[id]/page.tsx index 79948fb..3ab1363 100644 --- a/frontend/app/(app)/code/[id]/page.tsx +++ b/frontend/app/(app)/code/[id]/page.tsx @@ -1,5 +1,5 @@ import Navbar from "@/components/editor/navbar" -import { Room } from "@/components/editor/room" +import { Room } from "@/components/editor/live/room" import { Sandbox, User, UsersToSandboxes } from "@/lib/types" import { currentUser } from "@clerk/nextjs" import dynamic from "next/dynamic" @@ -49,12 +49,12 @@ export default async function CodePage({ params }: { params: { id: string } }) { return (
- {/* */} - -
- -
- {/*
*/} + + +
+ +
+
) } diff --git a/frontend/app/api/lb-auth/route.ts b/frontend/app/api/lb-auth/route.ts index 05bdb6a..ad480bc 100644 --- a/frontend/app/api/lb-auth/route.ts +++ b/frontend/app/api/lb-auth/route.ts @@ -23,7 +23,6 @@ export async function POST(request: NextRequest) { // userInfo is made available in Liveblocks presence hooks, e.g. useOthers const session = liveblocks.prepareSession(user.id, { userInfo: { - id: user.id, name: user.name, email: user.email, }, diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index 85d0eb0..72f5fad 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -11,7 +11,12 @@ import * as Y from "yjs" import LiveblocksProvider from "@liveblocks/yjs" import { MonacoBinding } from "y-monaco" import { Awareness } from "y-protocols/awareness" -import { useRoom } from "@/liveblocks.config" +import { + TypedLiveblocksProvider, + useRoom, + AwarenessList, + useSelf, +} from "@/liveblocks.config" import { ResizableHandle, @@ -36,6 +41,8 @@ import GenerateInput from "./generate" import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" import { User } from "@/lib/types" import { processFileType, validateName } from "@/lib/utils" +import { Cursors } from "./live/cursors" +import { Avatars } from "./live/avatars" export default function CodeEditor({ userData, @@ -63,6 +70,7 @@ export default function CodeEditor({ instance: monaco.editor.IEditorDecorationsCollection | undefined }>({ options: [], instance: undefined }) const [terminals, setTerminals] = useState([]) + const [provider, setProvider] = useState() const clerk = useClerk() const room = useRoom() @@ -268,6 +276,7 @@ export default function CodeEditor({ const yDoc = new Y.Doc() const yText = yDoc.getText("monaco") const yProvider: any = new LiveblocksProvider(room, yDoc) + setProvider(yProvider) const binding = new MonacoBinding( yText, @@ -481,10 +490,11 @@ export default function CodeEditor({ {tab.name} ))} +
{activeId === null ? ( <> @@ -494,33 +504,36 @@ export default function CodeEditor({
) : clerk.loaded ? ( - { - setTabs((prev) => - prev.map((tab) => - tab.id === activeId ? { ...tab, saved: false } : tab + <> + {provider ? : null} + { + setTabs((prev) => + prev.map((tab) => + tab.id === activeId ? { ...tab, saved: false } : tab + ) ) - ) - }} - options={{ - minimap: { - enabled: false, - }, - padding: { - bottom: 4, - top: 4, - }, - scrollBeyondLastLine: false, - fixedOverflowWidgets: true, - fontFamily: "var(--font-geist-mono)", - }} - theme="vs-dark" - value={activeFile ?? ""} - /> + }} + options={{ + minimap: { + enabled: false, + }, + padding: { + bottom: 4, + top: 4, + }, + scrollBeyondLastLine: false, + fixedOverflowWidgets: true, + fontFamily: "var(--font-geist-mono)", + }} + theme="vs-dark" + value={activeFile ?? ""} + /> + ) : null} diff --git a/frontend/components/editor/live/avatars.tsx b/frontend/components/editor/live/avatars.tsx new file mode 100644 index 0000000..2766083 --- /dev/null +++ b/frontend/components/editor/live/avatars.tsx @@ -0,0 +1,21 @@ +import { useOthers, useSelf } from "@/liveblocks.config" +import Avatar from "@/components/ui/avatar" + +export function Avatars() { + const users = useOthers() + const currentUser = useSelf() + + return ( +
+ {users.map(({ connectionId, info }) => { + return + })} + + {currentUser && ( +
+ +
+ )} +
+ ) +} diff --git a/frontend/components/editor/live/cursors.tsx b/frontend/components/editor/live/cursors.tsx new file mode 100644 index 0000000..c86dd47 --- /dev/null +++ b/frontend/components/editor/live/cursors.tsx @@ -0,0 +1,57 @@ +import { useEffect, useMemo, useState } from "react" +import { + AwarenessList, + TypedLiveblocksProvider, + UserAwareness, + useSelf, +} from "@/liveblocks.config" + +export function Cursors({ yProvider }: { yProvider: TypedLiveblocksProvider }) { + // Get user info from Liveblocks authentication endpoint + const userInfo = useSelf((me) => me.info) + + const [awarenessUsers, setAwarenessUsers] = useState([]) + + useEffect(() => { + // Add user info to Yjs awareness + const localUser: UserAwareness["user"] = userInfo + yProvider.awareness.setLocalStateField("user", localUser) + + // On changes, update `awarenessUsers` + function setUsers() { + setAwarenessUsers( + Array.from(yProvider.awareness.getStates()) as AwarenessList + ) + } + yProvider.awareness.on("change", setUsers) + setUsers() + + return () => { + yProvider.awareness.off("change", setUsers) + } + }, [yProvider]) + + // Insert awareness info into cursors with styles + const styleSheet = useMemo(() => { + let cursorStyles = "" + + for (const [clientId, client] of awarenessUsers) { + if (client?.user) { + cursorStyles += ` + .yRemoteSelection-${clientId}, + .yRemoteSelectionHead-${clientId} { + --user-color: ${"#FF0000"}; + } + + .yRemoteSelectionHead-${clientId}::after { + content: "${client.user.name}"; + } + ` + } + } + + return { __html: cursorStyles } + }, [awarenessUsers]) + + return