mostly done liveblocks integration

This commit is contained in:
Ishaan Dey 2024-05-03 14:27:45 -07:00
parent 0f1654e3dd
commit a18bcf9c14
7 changed files with 147 additions and 49 deletions

View File

@ -1,5 +1,5 @@
import Navbar from "@/components/editor/navbar" 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 { Sandbox, User, UsersToSandboxes } from "@/lib/types"
import { currentUser } from "@clerk/nextjs" import { currentUser } from "@clerk/nextjs"
import dynamic from "next/dynamic" import dynamic from "next/dynamic"
@ -49,12 +49,12 @@ export default async function CodePage({ params }: { params: { id: string } }) {
return ( return (
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background"> <div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
{/* <Room> */} <Room id={sandboxId}>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} /> <Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow"> <div className="w-screen flex grow">
<CodeEditor userData={userData} sandboxId={sandboxId} /> <CodeEditor userData={userData} sandboxId={sandboxId} />
</div> </div>
{/* </Room> */} </Room>
</div> </div>
) )
} }

View File

@ -23,7 +23,6 @@ export async function POST(request: NextRequest) {
// userInfo is made available in Liveblocks presence hooks, e.g. useOthers // userInfo is made available in Liveblocks presence hooks, e.g. useOthers
const session = liveblocks.prepareSession(user.id, { const session = liveblocks.prepareSession(user.id, {
userInfo: { userInfo: {
id: user.id,
name: user.name, name: user.name,
email: user.email, email: user.email,
}, },

View File

@ -11,7 +11,12 @@ import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs" import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco" import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness" import { Awareness } from "y-protocols/awareness"
import { useRoom } from "@/liveblocks.config" import {
TypedLiveblocksProvider,
useRoom,
AwarenessList,
useSelf,
} from "@/liveblocks.config"
import { import {
ResizableHandle, ResizableHandle,
@ -36,6 +41,8 @@ import GenerateInput from "./generate"
import { TFile, TFileData, TFolder, TTab } from "./sidebar/types" import { TFile, TFileData, TFolder, TTab } from "./sidebar/types"
import { User } from "@/lib/types" import { User } from "@/lib/types"
import { processFileType, validateName } from "@/lib/utils" import { processFileType, validateName } from "@/lib/utils"
import { Cursors } from "./live/cursors"
import { Avatars } from "./live/avatars"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -63,6 +70,7 @@ export default function CodeEditor({
instance: monaco.editor.IEditorDecorationsCollection | undefined instance: monaco.editor.IEditorDecorationsCollection | undefined
}>({ options: [], instance: undefined }) }>({ options: [], instance: undefined })
const [terminals, setTerminals] = useState<string[]>([]) const [terminals, setTerminals] = useState<string[]>([])
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
const clerk = useClerk() const clerk = useClerk()
const room = useRoom() const room = useRoom()
@ -268,6 +276,7 @@ export default function CodeEditor({
const yDoc = new Y.Doc() const yDoc = new Y.Doc()
const yText = yDoc.getText("monaco") const yText = yDoc.getText("monaco")
const yProvider: any = new LiveblocksProvider(room, yDoc) const yProvider: any = new LiveblocksProvider(room, yDoc)
setProvider(yProvider)
const binding = new MonacoBinding( const binding = new MonacoBinding(
yText, yText,
@ -481,10 +490,11 @@ export default function CodeEditor({
{tab.name} {tab.name}
</Tab> </Tab>
))} ))}
<Avatars />
</div> </div>
<div <div
ref={editorContainerRef} ref={editorContainerRef}
className="grow w-full overflow-hidden rounded-md" className="grow w-full overflow-hidden rounded-md relative"
> >
{activeId === null ? ( {activeId === null ? (
<> <>
@ -494,33 +504,36 @@ export default function CodeEditor({
</div> </div>
</> </>
) : clerk.loaded ? ( ) : clerk.loaded ? (
<Editor <>
height="100%" {provider ? <Cursors yProvider={provider} /> : null}
language={editorLanguage} <Editor
beforeMount={handleEditorWillMount} height="100%"
onMount={handleEditorMount} language={editorLanguage}
onChange={(value) => { beforeMount={handleEditorWillMount}
setTabs((prev) => onMount={handleEditorMount}
prev.map((tab) => onChange={(value) => {
tab.id === activeId ? { ...tab, saved: false } : tab setTabs((prev) =>
prev.map((tab) =>
tab.id === activeId ? { ...tab, saved: false } : tab
)
) )
) }}
}} options={{
options={{ minimap: {
minimap: { enabled: false,
enabled: false, },
}, padding: {
padding: { bottom: 4,
bottom: 4, top: 4,
top: 4, },
}, scrollBeyondLastLine: false,
scrollBeyondLastLine: false, fixedOverflowWidgets: true,
fixedOverflowWidgets: true, fontFamily: "var(--font-geist-mono)",
fontFamily: "var(--font-geist-mono)", }}
}} theme="vs-dark"
theme="vs-dark" value={activeFile ?? ""}
value={activeFile ?? ""} />
/> </>
) : null} ) : null}
</div> </div>
</ResizablePanel> </ResizablePanel>

View File

@ -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 (
<div className="flex">
{users.map(({ connectionId, info }) => {
return <Avatar key={connectionId} name={info.name} />
})}
{currentUser && (
<div className="relative ml-8 first:ml-0">
<Avatar name={currentUser.info.name} />
</div>
)}
</div>
)
}

View File

@ -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<AwarenessList>([])
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 <style dangerouslySetInnerHTML={styleSheet} />
}

View File

@ -1,15 +1,18 @@
"use client" "use client"
import { RoomProvider } from "@/liveblocks.config" import { RoomProvider } from "@/liveblocks.config"
import { useSearchParams } from "next/navigation"
import { ClientSideSuspense } from "@liveblocks/react" import { ClientSideSuspense } from "@liveblocks/react"
export function Room({ children }: { children: React.ReactNode }) { export function Room({
// const roomId = useExampleRoomId("liveblocks:examples:nextjs-yjs-monaco"); id,
children,
}: {
id: string
children: React.ReactNode
}) {
return ( return (
<RoomProvider <RoomProvider
id={"roomId"} id={id}
initialPresence={{ initialPresence={{
cursor: null, cursor: null,
}} }}

View File

@ -69,8 +69,11 @@ type Storage = {
// provided by your own custom auth back end (if used). Useful for data that // provided by your own custom auth back end (if used). Useful for data that
// will not change during a session, like a user's name or avatar. // will not change during a session, like a user's name or avatar.
type UserMeta = { type UserMeta = {
// id?: string, // Accessible through `user.id` id: string
// info?: Json, // Accessible through `user.info` info: {
name: string
email: string
}
} }
// Optionally, the type of custom events broadcast and listened to in this // Optionally, the type of custom events broadcast and listened to in this
@ -88,6 +91,12 @@ export type ThreadMetadata = {
// time: number; // time: number;
} }
export type UserAwareness = {
user?: UserMeta["info"]
}
export type AwarenessList = [number, UserAwareness][]
// Room-level hooks, use inside `RoomProvider` // Room-level hooks, use inside `RoomProvider`
export const { export const {
suspense: { suspense: {
@ -131,8 +140,8 @@ export const {
useUpdateRoomNotificationSettings, useUpdateRoomNotificationSettings,
// These hooks can be exported from either context // These hooks can be exported from either context
// useUser, useUser,
// useRoomInfo useRoomInfo,
}, },
} = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>( } = createRoomContext<Presence, Storage, UserMeta, RoomEvent, ThreadMetadata>(
client client
@ -146,10 +155,6 @@ export const {
useMarkAllInboxNotificationsAsRead, useMarkAllInboxNotificationsAsRead,
useInboxNotifications, useInboxNotifications,
useUnreadInboxNotificationsCount, useUnreadInboxNotificationsCount,
// These hooks can be exported from either context
useUser,
useRoomInfo,
}, },
} = createLiveblocksContext<UserMeta, ThreadMetadata>(client) } = createLiveblocksContext<UserMeta, ThreadMetadata>(client)