Skip to content

Commit 1822208

Browse files
feat: Comments (#1376)
* wip: better liveblocks support * misc fixes * misc * revert minimal * simplify setup * update config * fix * fix * markview * cleanup * wip * wip * wip * wip * misc * add threadstore tests * document recommended auth rules * resolve * basic userstore impl * user auth * Big comments UX WIP * Updated reactions UX * change reaction implementation * reactions improvements * small cleanup * cleanups + mark some todos * comments * fix locales * fix build * cleanup + sample * fix build * fix lint * disable liveblocks for now * lint * fix linkify warning + make toggle editable comment * fix content reset bug * Implemented PR feedback * fix placeholder * clean comment editor * fix build * fix placeholders * - Adjusted comment spacing - Changed emoji icon - Changed "More actions" dropdown alignment - Made toolbar button tooltips with spaces not become multi-line - Made "More actions" and emoji buttons hide tooltips when dropdown is open - Made emoji picker close when emoji is picked * Implemented PR feedback * fix build * implement TipTapThreadStore * address feedback * add comment * wip * add autofocus * Simplified tooltip + popover interaction * feat: ShadCN comments (#1445) * Added ShadCN comments implementation * Removed unneeded state * Fixed menu stealing focus * Updated screenshots * Added ariakit comments implementation (#1448) * change reaction auth * make emoji optional * fix bug * Extracted reaction badge WIP * fix useUsers * Fixed formatting toolbar not showing up when editor is non-editable * Fixed reaction badge tooltip line breaks and made leaving comment not hide popovers/menus in it * Improved badge UX and made reactions hide when editing * Fixed new comments sometimes not being selectable * remove unused files * rest thread store * fix copy/paste * add shift * add docs * update docs * revert liveblocks, will be separate PR * clean lockfile * fix some todos * adress number of comments * address more comments * address some comments * clean commentsplugin * ForceSelectionVisible * fix floatingcomposercontroller * fix unit test * close threads on esc * remove debugger * small fix * Fixed badge styles on docs * Fixed badge click handler * Added remaining UX fixes from Mantine to Ariakit/ShadCN * Externalized strings * Small styling fix * Small styling fix * Fix lint * Fixed side menu regression issue * Implemented PR feedback * Implemented PR feedback * Updated emoji picker screenshots * Revert "Updated emoji picker screenshots" This reverts commit a647ec3. * Updated `package-lock.json` * Fixed `no` locale * Updated `package-lock.json` * Added temp test pass --------- Co-authored-by: matthewlipski <[email protected]> Co-authored-by: Matthew Lipski <[email protected]>
1 parent 16a00e9 commit 1822208

File tree

130 files changed

+6145
-141
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

130 files changed

+6145
-141
lines changed

Diff for: docs/components/example/styles.css

+19-15
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
:focus-visible {
2-
box-shadow: unset !important;
2+
box-shadow: unset !important;
33
}
44

55
.demo .nextra-code-block pre {
6-
background-color: transparent !important;
7-
margin: 0 !important;
8-
padding: 0 !important;
6+
background-color: transparent !important;
7+
margin: 0 !important;
8+
padding: 0 !important;
99
}
1010

1111
.demo .nextra-code-block code > span {
12-
padding: 0 !important;
12+
padding: 0 !important;
1313
}
1414

15-
.demo .bn-container,
15+
.demo .bn-container:not(.bn-comment-editor),
1616
.demo .bn-editor {
17-
height: 100%;
17+
height: 100%;
18+
}
19+
20+
.demo .bn-container:not(.bn-comment-editor) .bn-editor {
21+
height: 100%;
1822
}
1923

2024
.demo .bn-editor {
21-
overflow: auto;
22-
padding-block: 1rem;
25+
overflow: auto;
26+
padding-block: 1rem;
2327
}
2428

2529
.demo-contents a {
26-
color: revert;
27-
text-decoration: revert;
30+
color: revert;
31+
text-decoration: revert;
2832
}
2933

3034
.demo code.bn-inline-content {
31-
font-size: 1em;
32-
line-height: 1.5;
33-
display: block;
34-
}
35+
font-size: 1em;
36+
line-height: 1.5;
37+
display: block;
38+
}

Diff for: docs/next.config.mjs

+5
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ const nextConfig = withAnalyzer(
123123
destination: "/examples/basic/default-blocks",
124124
permanent: true,
125125
},
126+
{
127+
source: "/docs/advanced/real-time-collaboration",
128+
destination: "/docs/collaboration",
129+
permanent: true,
130+
},
126131
],
127132
experimental: {
128133
externalDir: true,

Diff for: docs/pages/docs/_meta.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"styling-theming": "Styling & Theming",
77
"ui-components": "UI Components",
88
"custom-schemas": "Custom Schemas",
9+
"collaboration": "Collaboration",
910
"advanced": "Advanced",
1011
"discord-link": {
1112
"title": "Community ↗",

Diff for: docs/pages/docs/collaboration.mdx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
title: Collaboration
3+
description: Learn how to create multiplayer experiences with BlockNote
4+
---
5+
6+
# Collaboration (advanced)
7+
8+
BlockNote supports multi-user collaborative document editing.
9+
10+
- [Real-time collaboration](/docs/collaboration/real-time-collaboration)
11+
- [Comments](/docs/collaboration/comments)

Diff for: docs/pages/docs/collaboration/_meta.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"real-time-collaboration": "Real-time collaboration",
3+
"comments": "Comments"
4+
}

Diff for: docs/pages/docs/collaboration/comments.mdx

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
---
2+
title: Comments
3+
description: Learn how to enable comments in your BlockNote editor
4+
imageTitle: Comments
5+
---
6+
7+
import { Example } from "@/components/example";
8+
9+
# Comments
10+
11+
BlockNote supports Comments, Comment Threads (replies) and emoji reactions out of the box.
12+
13+
To enable comments in your editor, you need to:
14+
15+
- provide a `resolveUsers` so BlockNote can retrieve and display user information (names and avatars).
16+
- provide a `ThreadStore` so BlockNote can store and retrieve comment threads.
17+
- enable real-time collaboration (see [Real-time collaboration](/docs/collaboration/real-time-collaboration))
18+
19+
```tsx
20+
const editor = useCreateBlockNote({
21+
resolveUsers: async (userIds: string[]) => {
22+
// return user information for the given userIds (see below)
23+
},
24+
comments: {
25+
threadStore: yourThreadStore, // see below
26+
},
27+
// ...
28+
collaboration: {
29+
// ... // see real-time collaboration docs
30+
},
31+
});
32+
```
33+
34+
**Demo**
35+
36+
<Example name="collaboration/comments" />
37+
38+
## ThreadStores
39+
40+
A ThreadStore is used to store and retrieve comment threads. BlockNote is backend agnostic, so you can use any database or backend to store the threads.
41+
BlockNote comes with several built-in ThreadStore implementations:
42+
43+
### `YjsThreadStore`
44+
45+
The `YjsThreadStore` provides direct Yjs-based storage for comments, storing thread data directly in the Yjs document. This implementation is ideal for simple collaborative setups where all users have write access to the document.
46+
47+
```tsx
48+
import { YjsThreadStore } from "@blocknote/core";
49+
50+
const threadStore = new YjsThreadStore(
51+
userId, // The active user's ID
52+
yDoc.getMap("threads"), // Y.Map to store threads
53+
new DefaultThreadStoreAuth(userId, "editor"), // Authorization information, see below
54+
);
55+
```
56+
57+
_Note: While this is the easiest to implement, it requires users to have write access to the Yjs document to leave comments. Also, without proper server-side validation, any user could technically modify other users' comments._
58+
59+
### `RESTYjsThreadStore`
60+
61+
The `RESTYjsThreadStore` combines Yjs storage with a REST API backend, providing secure comment management while maintaining real-time collaboration. This implementation is ideal when you have strong authentication requirements, but is a little more work to set up.
62+
63+
In this implementation, data is written to the Yjs document via a REST API which can handle access control. Data is still retrieved from the Yjs document directly (after it's been updated by the REST API), this way all comment information automatically syncs between clients using the existing collaboration provider.
64+
65+
```tsx
66+
import { RESTYjsThreadStore, DefaultThreadStoreAuth } from "@blocknote/core";
67+
68+
const threadStore = new RESTYjsThreadStore(
69+
"https://api.example.com/comments", // Base URL for the REST API
70+
{
71+
Authorization: "Bearer your-token", // Optional headers to add to requests
72+
},
73+
yDoc.getMap("threads"), // Y.Map to retrieve commend data from
74+
new DefaultThreadStoreAuth(userId, "editor"), // Authorization rules (see below)
75+
);
76+
```
77+
78+
An example implementation of the REST API can be found in the [example repository](https://github.com/TypeCellOS/BlockNote-demo-nextjs-hocuspocus).
79+
80+
_Note: Because writes are executed via a REST API, the `RESTYjsThreadStore` is not suitable for local-first applications that should be able to add and edit comments offline._
81+
82+
### `TiptapThreadStore`
83+
84+
The `TiptapThreadStore` integrates with Tiptap's collaboration provider for comment management. This implementation is designed specifically for use with Tiptap's collaborative editing features.
85+
86+
```tsx
87+
import { TiptapThreadStore, DefaultThreadStoreAuth } from "@blocknote/core";
88+
import { TiptapCollabProvider } from "@hocuspocus/provider";
89+
90+
// Create a TiptapCollabProvider (you probably have this already)
91+
const provider = new TiptapCollabProvider({
92+
name: "test",
93+
baseUrl: "https://collab.yourdomain.com",
94+
appId: "test",
95+
document: doc,
96+
});
97+
98+
// Create a TiptapThreadStore
99+
const threadStore = new TiptapThreadStore(
100+
userId, // The active user's ID
101+
provider, // Tiptap collaboration provider
102+
new DefaultThreadStoreAuth(userId, "editor"), // Authorization rules (see below)
103+
);
104+
```
105+
106+
### ThreadStoreAuth
107+
108+
The `ThreadStoreAuth` class defines the authorization rules for interacting with comments. Every ThreadStore implementation requires a `ThreadStoreAuth` instance. BlockNote uses the `ThreadStoreAuth` instance to deterine which interactions are allowed for the current user (for example, whether they can create a new comment, edit or delete a comment, etc.).
109+
110+
The `DefaultThreadStoreAuth` class provides a basic implementation of the `ThreadStoreAuth` class. It takes a user ID and a role ("comment" or "editor") and implements the rules. See the [source code](https://github.com/TypeCellOS/BlockNote/blob/main/packages/core/src/extensions/Comments/threadstore/DefaultThreadStoreAuth.ts) for more details.
111+
112+
_Note: The `ThreadStoreAuth` only used to show / hide options in the UI. To secure comment related data, you still need to implement your own server-side validation (e.g. using `RESTYjsThreadStore` and a secure REST API)._
113+
114+
## `resolveUsers` function
115+
116+
When a user interacts with a comment, the data is stored in the ThreadStore, along with the active user ID (as specified when initiating the ThreadStore).
117+
118+
To display comments, BlockNote needs to retrieve user information (such as the username and avatar) based on the user ID. To do this, you need to provide a `resolveUsers` function in the editor options.
119+
120+
This function is called with an array of user IDs, and should return an array of `User` objects in the same order.
121+
122+
```tsx
123+
type User = {
124+
id: string;
125+
username: string;
126+
avatarUrl: string;
127+
};
128+
129+
async function myResolveUsers(userIds: string[]): Promise<User[]> {
130+
// fetch user information from your database / backend
131+
// and return an array of User objects
132+
133+
return await callYourBackend(userIds); //
134+
135+
// Return a list of users
136+
return users;
137+
}
138+
```

Diff for: docs/pages/docs/editor-basics/setup.mdx

+10-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function useCreateBlockNote(
2121
type BlockNoteEditorOptions = {
2222
animations?: boolean;
2323
collaboration?: CollaborationOptions;
24+
comments?: CommentsConfig;
2425
defaultStyles?: boolean;
2526
dictionary?: Dictionary;
2627
disableExtensions?: string[];
@@ -50,6 +51,8 @@ The hook takes two optional parameters:
5051

5152
`collaboration`: Options for enabling real-time collaboration. See [Real-time Collaboration](/docs/advanced/real-time-collaboration) for more info.
5253

54+
`comments`: Configuration for the comments feature, requires a `threadStore`. See [Comments](/docs/collaboration/comments) for more.
55+
5356
`defaultStyles`: Whether to use the default font and reset the styles of `<p>`, `<li>`, `<h1>`, etc. elements that are used in BlockNote. Defaults to true if undefined.
5457

5558
`dictionary`: Provide strings for localization. See the [Localization / i18n example](/examples/basic/localization) and [Custom Placeholders](/examples/basic/custom-placeholder).
@@ -62,15 +65,17 @@ The hook takes two optional parameters:
6265

6366
`initialContent:` The content that should be in the editor when it's created, represented as an array of [Partial Blocks](/docs/manipulating-blocks#partial-blocks).
6467

65-
`resolveFileUrl:` An async function that fetches the download URL of a file from an initial URL.
68+
`resolveFileUrl:` Function to resolve file URLs for display/download. Useful for creating authenticated URLs or implementing custom protocols.
69+
70+
`resolveUsers`: Function to resolve user information for comments. See [Comments](/docs/collaboration/comments) for more.
6671

6772
`schema`: The editor schema if you want to extend your editor with custom blocks, styles, or inline content [Custom Schemas](/docs/custom-schemas).
6873

6974
`setIdAttribute`: Whether to render an `id` HTML attribute on blocks as well as a `data-id` attribute. Defaults to `false`.
7075

7176
`sideMenuDetection`: Determines whether the mouse cursor position is locked to the editor bounding box for showing the [Block Side Menu](/docs/ui-components/side-menu) and block drag & drop. When set to `viewport`, the Side Menu will be shown next to the nearest block to the cursor, regardless of where it is in the viewport. Dropping blocks will also be locked to the editor bounding box. Otherwise, the Side Menu will only be shown when the cursor is within the editor bounds, and blocks can only be dropped when hovering the editor. In order to use multiple editors, must be set to `editor`. Defaults to `viewport`.
7277

73-
`tabBehavior`: Determines whether pressing the tab key should navigate the UI for keyboard accessibility or indent the current block. Defaults to `prefer-navigate-ui`.
78+
`tabBehavior`: Determines whether pressing the tab key should navigate toolbars for keyboard accessibility. When set to `"prefer-navigate-ui`, the user can navigate toolbars using Tab. Pressing Escape re-focuses the editor, and Tab now indents blocks. `"prefer-indent"` causes Tab to always indent blocks. Defaults to `prefer-navigate-ui`.
7479

7580
`trailingBlock`: An option which user can pass with `false` value to disable the automatic creation of a trailing new block on the next line when the user types or edits any block. Defaults to `true` if undefined.
7681

@@ -125,6 +130,7 @@ export type BlockNoteViewProps = {
125130
emojiPicker?: boolean;
126131
filePanel?: boolean;
127132
tableHandles?: boolean;
133+
comments?: boolean;
128134
children?:
129135
} & HTMLAttributes<HTMLDivElement>;
130136
```
@@ -153,6 +159,8 @@ export type BlockNoteViewProps = {
153159

154160
`tableHandles`: Whether the Table Handles should be enabled.
155161

162+
`comments`: Whether the default comments UI feature should be enabled.
163+
156164
`children`: Pass child elements to the `BlockNoteView` to create or customize toolbars, menus, or other UI components. See [UI Components](/docs/ui-components) for more.
157165

158166
Additional props passed are forwarded to the HTML `div` element BlockNote renders internally.

Diff for: examples/01-basic/01-minimal/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useCreateBlockNote } from "@blocknote/react";
55

66
export default function App() {
77
// Creates a new editor instance.
8-
const editor = useCreateBlockNote();
8+
const editor = useCreateBlockNote({});
99

1010
// Renders the editor instance using a React component.
1111
return <BlockNoteView editor={editor} />;

Diff for: examples/01-basic/02-block-objects/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Block } from "@blocknote/core";
22
import "@blocknote/core/fonts/inter.css";
3-
import { useCreateBlockNote } from "@blocknote/react";
43
import { BlockNoteView } from "@blocknote/mantine";
54
import "@blocknote/mantine/style.css";
5+
import { useCreateBlockNote } from "@blocknote/react";
66
import { useState } from "react";
77

88
import "./styles.css";

Diff for: examples/07-collaboration/01-partykit/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ In this example, we use PartyKit to let multiple users collaborate on a single B
66

77
**Relevant Docs:**
88

9-
- [PartyKit](/docs/advanced/real-time-collaboration#partykit)
109
- [Editor Setup](/docs/editor-basics/setup)
10+
- [PartyKit](/docs/collaboration/real-time-collaboration#partykit)

Diff for: examples/07-collaboration/02-liveblocks/App.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "@blocknote/core/fonts/inter.css";
2-
import { useCreateBlockNote } from "@blocknote/react";
32
import { BlockNoteView } from "@blocknote/mantine";
43
import "@blocknote/mantine/style.css";
4+
import { useCreateBlockNote } from "@blocknote/react";
55
import { createClient } from "@liveblocks/client";
66
import LiveblocksProvider from "@liveblocks/yjs";
77
import * as Y from "yjs";

Diff for: examples/07-collaboration/02-liveblocks/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ In this example, we use Liveblocks to let multiple users collaborate on a single
66

77
**Relevant Docs:**
88

9-
- [Liveblocks](/docs/advanced/real-time-collaboration#liveblocks)
109
- [Editor Setup](/docs/editor-basics/setup)
10+
- [Liveblocks](/docs/collaboration/real-time-collaboration#liveblocks)

Diff for: examples/07-collaboration/03-y-sweet/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ In this example, we use Y-Sweet to let multiple users collaborate on a single Bl
66

77
**Relevant Docs:**
88

9-
- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)
109
- [Editor Setup](/docs/editor-basics/setup)
10+
- [Real-time collaboration](/docs/collaboration/real-time-collaboration)
11+
- [Y-Sweet on Jamsocket](https://docs.jamsocket.com/y-sweet/tutorials/blocknote)
+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"playground": true,
3+
"docs": true,
4+
"author": "yousefed",
5+
"tags": ["Advanced", "Comments", "Collaboration"],
6+
"dependencies": {
7+
"@y-sweet/react": "^0.6.3",
8+
"@mantine/core": "^7.10.1"
9+
}
10+
}

0 commit comments

Comments
 (0)