Examples
Collaboration
Threads Sidebar

Threads Sidebar

In this example, comments are enabled on the editor, but are now shown in a separate sidebar to the side.

Try it out: Click the "Add comment" button in the Formatting Toolbar to add a comment!

Relevant Docs:

"use client";
 
import {
  DefaultThreadStoreAuth,
  YjsThreadStore,
} from "@blocknote/core/comments";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import {
  BlockNoteViewEditor,
  ThreadsSidebar,
  useCreateBlockNote,
} from "@blocknote/react";
import { YDocProvider, useYDoc, useYjsProvider } from "@y-sweet/react";
import { useMemo, useState } from "react";
 
import { SettingsSelect } from "./SettingsSelect.js";
import { HARDCODED_USERS, MyUserType, getRandomColor } from "./userdata.js";
 
import "./style.css";
 
// The resolveUsers function fetches information about your users
// (e.g. their name, avatar, etc.). Usually, you'd fetch this from your
// own database or user management system.
// Here, we just return the hardcoded users (from userdata.ts)
async function resolveUsers(userIds: string[]) {
  // fake a (slow) network request
  await new Promise((resolve) => setTimeout(resolve, 1000));
 
  return HARDCODED_USERS.filter((user) => userIds.includes(user.id));
}
 
// This follows the Y-Sweet example to setup a collabotive editor
// (but of course, you also use other collaboration providers
// see the docs for more information)
export default function App() {
  const docId = "my-blocknote-document-with-comments-1";
 
  return (
    <YDocProvider
      docId={docId}
      authEndpoint="https://demos.y-sweet.dev/api/auth">
      <Document />
    </YDocProvider>
  );
}
 
function Document() {
  const [activeUser, setActiveUser] = useState<MyUserType>(HARDCODED_USERS[0]);
  const [commentFilter, setCommentFilter] = useState<
    "open" | "resolved" | undefined
  >("open");
  const [commentSort, setCommentSort] = useState<
    "position" | "recent-activity" | "oldest"
  >("position");
 
  const provider = useYjsProvider();
 
  // take the Y.Doc collaborative document from Y-Sweet
  const doc = useYDoc();
 
  // setup the thread store which stores / and syncs thread / comment data
  const threadStore = useMemo(() => {
    // (alternative, use TiptapCollabProvider)
    // const provider = new TiptapCollabProvider({
    //   name: "test",
    //   baseUrl: "https://collab.yourdomain.com",
    //   appId: "test",
    //   document: doc,
    // });
    // return new TiptapThreadStore(
    //   activeUser.id,
    //   provider,
    //   new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
    // );
    return new YjsThreadStore(
      activeUser.id,
      doc.getMap("threads"),
      new DefaultThreadStoreAuth(activeUser.id, activeUser.role)
    );
  }, [doc, activeUser]);
 
  // setup the editor with comments and collaboration
  const editor = useCreateBlockNote(
    {
      resolveUsers,
      comments: {
        threadStore,
      },
      collaboration: {
        provider,
        fragment: doc.getXmlFragment("blocknote"),
        user: { color: getRandomColor(), name: activeUser.username },
      },
    },
    [activeUser, threadStore]
  );
 
  return (
    <BlockNoteView
      className={"sidebar-comments-main-container"}
      editor={editor}
      editable={activeUser.role === "editor"}
      // In other examples, `BlockNoteView` renders both editor element itself,
      // and the container element which contains the necessary context for
      // BlockNote UI components. However, in this example, we want more control
      // over the rendering of the editor, so we set `renderEditor` to `false`.
      // Now, `BlockNoteView` will only render the container element, and we can
      // render the editor element anywhere we want using `BlockNoteEditorView`.
      renderEditor={false}
      // We also disable the default rendering of comments in the editor, as we
      // want to render them in the `ThreadsSidebar` component instead.
      comments={false}>
      {/* We place the editor, the sidebar, and any settings selects within
      `BlockNoteView` as they use BlockNote UI components and need the context
      for them. */}
      <div className={"editor-layout-wrapper"}>
        <div className={"editor-section"}>
          <h1>Editor</h1>
          <div className={"settings"}>
            <SettingsSelect
              label={"User"}
              items={HARDCODED_USERS.map((user) => ({
                text: `${user.username} (${
                  user.role === "editor" ? "Editor" : "Commenter"
                })`,
                icon: null,
                onClick: () => setActiveUser(user),
                isSelected: user.id === activeUser.id,
              }))}
            />
          </div>
          {/* Because we set `renderEditor` to false, we can now manually place
          `BlockNoteViewEditor` (the actual editor component) in its own
          section below the user settings select. */}
          <BlockNoteViewEditor />
        </div>
      </div>
      {/* We also place the `ThreadsSidebar` component in its own section,
      along with settings for filtering and sorting. */}
      <div className={"threads-sidebar-section"}>
        <h1>Comments</h1>
        <div className={"settings"}>
          <SettingsSelect
            label={"Filter"}
            items={[
              {
                text: "All",
                icon: null,
                onClick: () => setCommentFilter(undefined),
                isSelected: commentFilter === undefined,
              },
              {
                text: "Open",
                icon: null,
                onClick: () => setCommentFilter("open"),
                isSelected: commentFilter === "open",
              },
              {
                text: "Resolved",
                icon: null,
                onClick: () => setCommentFilter("resolved"),
                isSelected: commentFilter === "resolved",
              },
            ]}
          />
          <SettingsSelect
            label={"Sort"}
            items={[
              {
                text: "Position",
                icon: null,
                onClick: () => setCommentSort("position"),
                isSelected: commentSort === "position",
              },
              {
                text: "Recent activity",
                icon: null,
                onClick: () => setCommentSort("recent-activity"),
                isSelected: commentSort === "recent-activity",
              },
              {
                text: "Oldest",
                icon: null,
                onClick: () => setCommentSort("oldest"),
                isSelected: commentSort === "oldest",
              },
            ]}
          />
        </div>
        <ThreadsSidebar filter={commentFilter} sort={commentSort} />
      </div>
    </BlockNoteView>
  );
}