mostly done liveblocks integration
This commit is contained in:
parent
0f1654e3dd
commit
a18bcf9c14
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -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,6 +504,8 @@ export default function CodeEditor({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : clerk.loaded ? (
|
) : clerk.loaded ? (
|
||||||
|
<>
|
||||||
|
{provider ? <Cursors yProvider={provider} /> : null}
|
||||||
<Editor
|
<Editor
|
||||||
height="100%"
|
height="100%"
|
||||||
language={editorLanguage}
|
language={editorLanguage}
|
||||||
@ -521,6 +533,7 @@ export default function CodeEditor({
|
|||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
value={activeFile ?? ""}
|
value={activeFile ?? ""}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
21
frontend/components/editor/live/avatars.tsx
Normal file
21
frontend/components/editor/live/avatars.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
57
frontend/components/editor/live/cursors.tsx
Normal file
57
frontend/components/editor/live/cursors.tsx
Normal 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} />
|
||||||
|
}
|
@ -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,
|
||||||
}}
|
}}
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user