diff --git a/bun.lockb b/bun.lockb index e53746e..f4978c2 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index c7e6f8f..024b3e8 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,12 @@ "@lexical/rich-text": "^0.19.0", "@lexical/selection": "^0.19.0", "@lexical/utils": "^0.19.0", - "@liveblocks/client": "^2.11.0", - "@liveblocks/node": "^2.11.0", - "@liveblocks/react": "^2.11.0", - "@liveblocks/react-lexical": "^2.11.0", - "@liveblocks/react-tiptap": "^2.14.0", - "@liveblocks/react-ui": "^2.11.0", + "@liveblocks/client": "^2.12.2", + "@liveblocks/node": "^2.12.2", + "@liveblocks/react": "^2.12.2", + "@liveblocks/react-lexical": "^2.12.2", + "@liveblocks/react-tiptap": "^2.12.2", + "@liveblocks/react-ui": "^2.12.2", "@mantine/hooks": "^7.15.0", "@nextui-org/react": "^2.6.8", "@pinecone-database/pinecone": "^4.0.0", @@ -126,6 +126,7 @@ "tiptap-extension-resize-image": "^1.2.1", "uploadthing": "^7.4.0", "vaul": "^1.1.1", + "y-protocols": "^1.0.6", "zod": "^3.23.8", "zustand": "^5.0.2" }, diff --git a/src/app/(dashboard)/workspaces/[workspaceId]/documents/page.tsx b/src/app/(dashboard)/workspaces/[workspaceId]/documents/page.tsx index 17c5488..9974743 100644 --- a/src/app/(dashboard)/workspaces/[workspaceId]/documents/page.tsx +++ b/src/app/(dashboard)/workspaces/[workspaceId]/documents/page.tsx @@ -1,3 +1,5 @@ +'use client'; + import { NextPage } from 'next'; import { diff --git a/src/app/(documents)/workspaces/[workspaceId]/documents/[documentId]/client.tsx b/src/app/(documents)/workspaces/[workspaceId]/documents/[documentId]/client.tsx index b5c6724..0f5e011 100644 --- a/src/app/(documents)/workspaces/[workspaceId]/documents/[documentId]/client.tsx +++ b/src/app/(documents)/workspaces/[workspaceId]/documents/[documentId]/client.tsx @@ -1,11 +1,9 @@ 'use client'; import { useGetDocument } from '@/common/api/documents'; -import { Editor, Navbar, Toolbar } from '@/common/components/documents'; +import { Editor, Navbar, Room, Toolbar } from '@/common/components/documents'; import { PageError, PageLoader } from '@/common/components/elements'; -// import { Room } from "./room"; - interface DocumentProps { documentId: string; } @@ -22,17 +20,17 @@ const DocumentClient: React.FC = ({ documentId }) => { } return ( - // -
-
- - -
-
- + +
+
+ + +
+
+ +
-
- // + ); }; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 008c315..8e2175a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,6 +2,8 @@ import type { Metadata } from 'next'; import { Inter as FontSans } from 'next/font/google'; import '@/common/styles/globals.scss'; +import '@liveblocks/react-tiptap/styles.css'; +import '@liveblocks/react-ui/styles.css'; import 'react-loading-skeleton/dist/skeleton.css'; import 'simplebar-react/dist/simplebar.min.css'; @@ -24,6 +26,20 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + const chatbotScript = ` + + +`; return ( {children} +
); diff --git a/src/common/components/documents/Avatars.tsx b/src/common/components/documents/Avatars.tsx new file mode 100644 index 0000000..f9563d6 --- /dev/null +++ b/src/common/components/documents/Avatars.tsx @@ -0,0 +1,62 @@ +'use client'; + +import { ClientSideSuspense } from '@liveblocks/react'; +import { useOthers, useSelf } from '@liveblocks/react/suspense'; + +import { Separator } from '@/common/components/ui/separator'; + +interface AvatarProps { + src: string; + name: string; +} + +const AVATAR_SIZE = 36; + +const Avatar: React.FC = ({ src, name }) => { + return ( +
+
+ {name} +
+ {name} +
+ ); +}; + +const AvatarStack: React.FC = () => { + const users = useOthers(); + const currentUser = useSelf(); + + if (users.length === 0) return null; + + return ( + <> +
+ {currentUser && ( +
+ +
+ )} +
+ {users.map(({ connectionId, info }) => { + return ( + + ); + })} +
+
+ + + ); +}; + +const Avatars: React.FC = () => ( + + + +); + +export default Avatars; diff --git a/src/common/components/documents/DocumentInput.tsx b/src/common/components/documents/DocumentInput.tsx index 34bb9e4..c188de7 100644 --- a/src/common/components/documents/DocumentInput.tsx +++ b/src/common/components/documents/DocumentInput.tsx @@ -1,6 +1,6 @@ 'use client'; -// import { useStatus } from '@liveblocks/react'; +import { useStatus } from '@liveblocks/react'; import { LoaderIcon } from 'lucide-react'; import { useRef, useState } from 'react'; import { BsCloudCheck, BsCloudSlash } from 'react-icons/bs'; @@ -14,7 +14,7 @@ interface DocumentInputProps { } const DocumentInput: React.FC = ({ title, id }) => { - // const status = useStatus(); + const status = useStatus(); const [value, setValue] = useState(title); const [isEditing, setIsEditing] = useState(false); diff --git a/src/common/components/documents/Editor.tsx b/src/common/components/documents/Editor.tsx index deca40a..6035aa5 100644 --- a/src/common/components/documents/Editor.tsx +++ b/src/common/components/documents/Editor.tsx @@ -1,5 +1,6 @@ 'use client'; +// import { useStorage } from '@liveblocks/react'; // import { useLiveblocksExtension } from '@liveblocks/react-tiptap'; import { Color } from '@tiptap/extension-color'; import FontFamily from '@tiptap/extension-font-family'; @@ -27,7 +28,6 @@ import { } from '@/common/libs/extensions'; import { Ruler } from '.'; -// import { Threads } from './threads'; interface EditorProps { initialContent?: string | undefined; @@ -121,7 +121,7 @@ const Editor: React.FC = ({ initialContent }) => {
- {/* */} + {/* */}
); diff --git a/src/common/components/documents/Inbox.tsx b/src/common/components/documents/Inbox.tsx new file mode 100644 index 0000000..1fd70bc --- /dev/null +++ b/src/common/components/documents/Inbox.tsx @@ -0,0 +1,70 @@ +/* eslint-disable simple-import-sort/imports */ +'use client'; + +import { ClientSideSuspense } from '@liveblocks/react'; +import { InboxNotification, InboxNotificationList } from '@liveblocks/react-ui'; +import { useInboxNotifications } from '@liveblocks/react/suspense'; +import { BellIcon } from 'lucide-react'; + +import { Button } from '@/common/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/common/components/ui/dropdown-menu'; +import { Separator } from '@/common/components/ui/separator'; + +const InboxMenu: React.FC = () => { + const { inboxNotifications } = useInboxNotifications(); + + return ( + <> + + + + + + {inboxNotifications.length > 0 ? ( + + {inboxNotifications.map((inboxNotification) => ( + + ))} + + ) : ( +
+ 无消息 +
+ )} +
+
+ + + ); +}; + +const Inbox: React.FC = () => ( + + + + + } + > + + +); + +export default Inbox; diff --git a/src/common/components/documents/Navbar.tsx b/src/common/components/documents/Navbar.tsx index bb7d87b..3c7c440 100644 --- a/src/common/components/documents/Navbar.tsx +++ b/src/common/components/documents/Navbar.tsx @@ -40,11 +40,7 @@ import { import { useEditorStore, useWorkspaceId } from '@/common/hooks'; import { type Document } from '@/common/types/documents'; -import { RemoveDialog, RenameDialog } from '.'; -import DocumentInput from './DocumentInput'; - -// import { Avatars } from './avatars'; -// import { Inbox } from './inbox'; +import { Avatars, DocumentInput, RemoveDialog, RenameDialog } from '.'; interface NavbarProps { data: Document; @@ -300,8 +296,8 @@ const Navbar: React.FC = ({ data }) => {
- {/* - */} + + {/* */}
diff --git a/src/common/components/documents/Room.tsx b/src/common/components/documents/Room.tsx new file mode 100644 index 0000000..a03d4e3 --- /dev/null +++ b/src/common/components/documents/Room.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { + ClientSideSuspense, + LiveblocksProvider, + RoomProvider, +} from '@liveblocks/react/suspense'; +import { useParams } from 'next/navigation'; +import { useEffect, useState } from 'react'; +import { toast } from 'sonner'; + +import { LEFT_MARGIN_DEFAULT, RIGHT_MARGIN_DEFAULT } from '@/common/constants'; +import { getUsers } from '@/common/libs/actions/document.action'; + +import { PageLoader } from '../elements'; + +interface RoomProps { + children: React.ReactNode; +} + +type User = { + id: string; + name: string; + avatar: string; + color: string; + email: string; +}; + +const Room: React.FC = ({ children }) => { + const params = useParams(); + + const [users, setUsers] = useState([]); + + const fetchUsers = async () => { + try { + const list = await getUsers(); + setUsers(list); + } catch { + toast.error('获取用户列表失败'); + } + }; + + useEffect(() => { + fetchUsers(); + }, [fetchUsers]); + + return ( + { + // const room = params.documentId as string; + + // const body = await getLiveBlocks(room); + + // return { token: body }; + // }} + // resolveUsers={({ userIds }) => { + // return userIds.map( + // (userId) => users.find((user) => user.id === userId) ?? undefined + // ); + // }} + // resolveMentionSuggestions={({ text }) => { + // let filteredUsers = users; + + // if (text) { + // filteredUsers = users.filter((user) => + // user.name.toLowerCase().includes(text.toLowerCase()) + // ); + // } + + // return filteredUsers?.map((user) => user.id); + // }} + // resolveRoomsInfo={async ({ roomIds }) => { + // const documents = await getDocuments(roomIds as string[]); + // return documents.map((document) => ({ + // id: document.$id, + // name: document.name, + // })); + // }} + > + + }> + {children} + + + + ); +}; + +export default Room; diff --git a/src/common/components/documents/TemplatesGallery.tsx b/src/common/components/documents/TemplatesGallery.tsx index f6f3cfe..ba17520 100644 --- a/src/common/components/documents/TemplatesGallery.tsx +++ b/src/common/components/documents/TemplatesGallery.tsx @@ -1,6 +1,5 @@ 'use client'; -// import { useMutation } from "convex/react"; import { useRouter } from 'next/navigation'; import { useState } from 'react'; diff --git a/src/common/components/documents/Threads.tsx b/src/common/components/documents/Threads.tsx new file mode 100644 index 0000000..ff95c3d --- /dev/null +++ b/src/common/components/documents/Threads.tsx @@ -0,0 +1,34 @@ +/* eslint-disable simple-import-sort/imports */ +import { + AnchoredThreads, + FloatingComposer, + FloatingThreads, +} from '@liveblocks/react-tiptap'; +import { ClientSideSuspense, useThreads } from '@liveblocks/react/suspense'; +import { Editor } from '@tiptap/react'; + +const Threads: React.FC<{ editor: Editor | null }> = ({ editor }) => ( + + + +); + +export default Threads; + +const ThreadsList: React.FC<{ editor: Editor | null }> = ({ editor }) => { + const { threads } = useThreads({ query: { resolved: false } }); + + return ( + <> +
+ +
+ + + + ); +}; diff --git a/src/common/components/documents/index.tsx b/src/common/components/documents/index.tsx index 88dc02b..cb9830b 100644 --- a/src/common/components/documents/index.tsx +++ b/src/common/components/documents/index.tsx @@ -1,13 +1,17 @@ +export { default as Avatars } from './Avatars'; export { default as DocumentInput } from './DocumentInput'; export { default as DocumentMenu } from './DocumentMenu'; export { default as DocumentRow } from './DocumentRow'; export { default as DocumentsTable } from './DocumentsTable'; export { default as Editor } from './Editor'; export { default as HomeNav } from './HomeNav'; +export { default as Inbox } from './Inbox'; export { default as Navbar } from './Navbar'; export { default as RemoveDialog } from './RemoveDialog'; export { default as RenameDialog } from './RenameDialog'; +export { default as Room } from './Room'; export { default as Ruler } from './Ruler'; export { default as SearchInput } from './SearchInput'; export { default as TemplatesGallery } from './TemplatesGallery'; +export { default as Threads } from './Threads'; export { default as Toolbar } from './Toolbar'; diff --git a/src/common/libs/actions/document.action.ts b/src/common/libs/actions/document.action.ts new file mode 100644 index 0000000..54b1699 --- /dev/null +++ b/src/common/libs/actions/document.action.ts @@ -0,0 +1,70 @@ +'use server'; + +import { Query } from 'node-appwrite'; + +import { DATABASE_ID, DOCUMENTS_ID, MEMBERS_ID } from '@/common/configs'; +import { type Document } from '@/common/types/documents'; +import { getUserColor } from '@/common/utils'; + +import { createSessionClient } from '../appwrite'; +import liveblocks from '../liveblocks'; + +export const getDocuments = async (ids: string[]) => { + const { databases } = await createSessionClient(); + + const mergeDocuments: Document[] = []; + + for (const roomId of ids) { + const documents = await databases.listDocuments( + DATABASE_ID, + DOCUMENTS_ID, + [Query.equal('roomId', roomId), Query.orderDesc('$createdAt')] + ); + mergeDocuments.push(...documents.documents); + } + + return mergeDocuments; +}; + +export const getUsers = async () => { + const { databases, account } = await createSessionClient(); + + const user = await account.get(); + + const members = await databases.listDocuments(DATABASE_ID, MEMBERS_ID, [ + Query.equal('userId', user.$id), + ]); + + const users = members.documents.map((user) => ({ + id: user.$id, + name: user.name, + avatar: user.name, + color: '', + email: user.email, + })); + + return users; +}; + +export const getLiveBlocks = async (room: string) => { + const { account } = await createSessionClient(); + + const user = await account.get(); + + const name = user.name; + const session = liveblocks.prepareSession(user.$id, { + userInfo: { + name, + avatar: name + ? name.charAt(0).toUpperCase() + : (user.email.charAt(0).toUpperCase() ?? 'U'), + color: getUserColor(user.$id), + id: user.$id, + email: user.email, + }, + }); + session.allow(room, session.FULL_ACCESS); + const { body } = await session.authorize(); + + return body; +}; diff --git a/src/server/documents.ts b/src/server/documents.ts index 3817e43..0c7b025 100644 --- a/src/server/documents.ts +++ b/src/server/documents.ts @@ -1,6 +1,7 @@ import { zValidator } from '@hono/zod-validator'; import { Hono } from 'hono'; import { ID, Query } from 'node-appwrite'; +import { z } from 'zod'; import { DATABASE_ID, DOCUMENTS_ID } from '@/common/configs'; import sessionMiddleware from '@/common/libs/session-middleware'; @@ -88,6 +89,28 @@ const Documents = new Hono() return c.json({ data: document }); }) + .get( + '/roomIds', + sessionMiddleware, + zValidator('query', z.object({ roomIds: z.string().array() })), + async (c) => { + const databases = c.get('databases'); + const { roomIds } = c.req.valid('query'); + + const mergeDocuments: Document[] = []; + + for (const roomId of roomIds) { + const documents = await databases.listDocuments( + DATABASE_ID, + DOCUMENTS_ID, + [Query.equal('roomId', roomId), Query.orderDesc('$createdAt')] + ); + mergeDocuments.push(...documents.documents); + } + + return c.json({ data: mergeDocuments }); + } + ) .post( '/', sessionMiddleware, @@ -122,6 +145,57 @@ const Documents = new Hono() return c.json({ data: document }); } ) + // .post( + // '/liveblocks-auth', + // sessionMiddleware, + // zValidator('form', z.object({ room: z.string(), workspaceId: z.string() })), + // async (c) => { + // const user = c.get('user'); + // const databases = c.get('databases'); + + // const { room, workspaceId } = c.req.valid('form'); + // const documents = await databases.listDocuments( + // DATABASE_ID, + // DOCUMENTS_ID, + // [Query.equal('roomId', room), Query.orderDesc('$createdAt')] + // ); + // if (!documents) { + // return c.json({ error: 'Unathorized' }, 401); + // } + // const document = documents.documents[0]; + + // const isOwner = document.workspaceId === workspaceId; + // if (!isOwner) { + // return c.json({ error: 'Unauthorized' }, 401); + // } + + // const member = await getMember({ + // databases, + // workspaceId, + // userId: user.$id, + // }); + // if (!member) { + // return c.json({ error: 'Unathorized' }, 401); + // } + + // const name = user.name; + // const session = liveblocks.prepareSession(user.$id, { + // userInfo: { + // name, + // avatar: name + // ? name.charAt(0).toUpperCase() + // : (user.email.charAt(0).toUpperCase() ?? 'U'), + // color: getUserColor(user.$id), + // id: user.$id, + // email: user.email, + // }, + // }); + // session.allow(room, session.FULL_ACCESS); + // const { body, status } = await session.authorize(); + + // return c.json({ data: body }, status as StatusCode); + // } + // ) .patch( '/:documentId', sessionMiddleware,