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
+}
diff --git a/frontend/components/editor/room.tsx b/frontend/components/editor/live/room.tsx
similarity index 61%
rename from frontend/components/editor/room.tsx
rename to frontend/components/editor/live/room.tsx
index 13ae1bd..5ba17c5 100644
--- a/frontend/components/editor/room.tsx
+++ b/frontend/components/editor/live/room.tsx
@@ -1,15 +1,18 @@
"use client"
import { RoomProvider } from "@/liveblocks.config"
-import { useSearchParams } from "next/navigation"
import { ClientSideSuspense } from "@liveblocks/react"
-export function Room({ children }: { children: React.ReactNode }) {
- // const roomId = useExampleRoomId("liveblocks:examples:nextjs-yjs-monaco");
-
+export function Room({
+ id,
+ children,
+}: {
+ id: string
+ children: React.ReactNode
+}) {
return (
(
client
@@ -146,10 +155,6 @@ export const {
useMarkAllInboxNotificationsAsRead,
useInboxNotifications,
useUnreadInboxNotificationsCount,
-
- // These hooks can be exported from either context
- useUser,
- useRoomInfo,
},
} = createLiveblocksContext(client)