mostly done liveblocks integration
This commit is contained in:
@ -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<string[]>([])
|
||||
const [provider, setProvider] = useState<TypedLiveblocksProvider>()
|
||||
|
||||
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}
|
||||
</Tab>
|
||||
))}
|
||||
<Avatars />
|
||||
</div>
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className="grow w-full overflow-hidden rounded-md"
|
||||
className="grow w-full overflow-hidden rounded-md relative"
|
||||
>
|
||||
{activeId === null ? (
|
||||
<>
|
||||
@ -494,33 +504,36 @@ export default function CodeEditor({
|
||||
</div>
|
||||
</>
|
||||
) : clerk.loaded ? (
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
setTabs((prev) =>
|
||||
prev.map((tab) =>
|
||||
tab.id === activeId ? { ...tab, saved: false } : tab
|
||||
<>
|
||||
{provider ? <Cursors yProvider={provider} /> : null}
|
||||
<Editor
|
||||
height="100%"
|
||||
language={editorLanguage}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleEditorMount}
|
||||
onChange={(value) => {
|
||||
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}
|
||||
</div>
|
||||
</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"
|
||||
|
||||
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 (
|
||||
<RoomProvider
|
||||
id={"roomId"}
|
||||
id={id}
|
||||
initialPresence={{
|
||||
cursor: null,
|
||||
}}
|
Reference in New Issue
Block a user