diff --git a/CHANGELOG.md b/CHANGELOG.md
index e296c49c750..469b3dc2929 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,18 @@
## vNEXT (not yet published)
+## v3.3.4
+
### `@liveblocks/client`
- Fix race condition where AI tools were not always executing. This could happen
when using `useSendAiMessage` first and then immediately opening the
`` afterwards.
+### `@liveblocks/react-tiptap`
+
+- Scroll thread annotations into view when a thread in `AnchoredThreads` is
+ selected, similarly to `@liveblocks/react-lexical`.
+
## v3.3.1
### `@liveblocks/react-ui`
diff --git a/assets/account-management/team-mfa-setup.png b/assets/account-management/team-mfa-setup.png
new file mode 100644
index 00000000000..1a0b48cd228
Binary files /dev/null and b/assets/account-management/team-mfa-setup.png differ
diff --git a/docs/pages/platform/account-management/mfa.mdx b/docs/pages/platform/account-management/mfa.mdx
new file mode 100644
index 00000000000..555836c62fb
--- /dev/null
+++ b/docs/pages/platform/account-management/mfa.mdx
@@ -0,0 +1,38 @@
+---
+meta:
+ title: "Multi-factor authentication"
+ parentTitle: "Platform"
+ description:
+ "Learn how to enable multi-factor authentication on your Liveblocks account."
+---
+
+
+ This feature is available as an add-on to Enterprise customers. Reach out to
+ [sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on your
+ account.
+
+
+_Multi-factor authentication_ (MFA) adds an additional layer of security to
+teams to manage their members by enforcing them to have an authenticator app.
+Does not apply to Single Sign-On members.
+
+## How it works
+
+When MFA is enabled, all team members will be required to have an authenticator
+app installed on their device. They will be prompted to enter a code from the
+app when they log in to the dashboard.
+
+
+
+
+
+## How to enable MFA
+
+MFA is available as an add-on to Enterprise customers, reach out to
+[sales@liveblocks.io](mailto:sales@liveblocks.io) to enable it on your account.
+After it’s enabled, it will work without configuration.
diff --git a/docs/pages/platform/account-management/saml.mdx b/docs/pages/platform/account-management/saml.mdx
index aa32c2bde6a..ddd7e53b639 100644
--- a/docs/pages/platform/account-management/saml.mdx
+++ b/docs/pages/platform/account-management/saml.mdx
@@ -36,9 +36,11 @@ automatically assign users to your team with a specific role.
/>
-### Configuring SSO
+## Configuring SSO
-1. To configure SSO for your team, you must be an owner of the team (see
+After SSO is enabled on your account, you can configure it for your team.
+
+1. To configure SSO, you must be an owner of the team (see
[how to manage team members](/docs/platform/account-management/manage-team-members)).
2. From your dashboard, ensure your team is selected in the scope selector.
3. Navigate to the “Settings” tab and select “Security & Privacy”.
@@ -55,7 +57,7 @@ automatically assign users to your team with a specific role.
/>
-### Enforcing SSO
+## Enforcing SSO
For additional security, SSO can be enforced for a team so that all team members
cannot access any team information unless their current session is authenticated
@@ -69,13 +71,13 @@ have access to it. If you would like all users signing up with SSO to be
automatically added to your team, please contact us at
[support@liveblocks.io](mailto:support@liveblocks.io).
-### Authenticating with SSO
+## Authenticating with SSO
Once you have configured SSO, your team members can use it to log in or sign up
to Liveblocks. They only have to enter their email, and they will be redirected
to the third-party authentication provider that you configured.
-### Supported providers
+## Supported providers
Liveblocks supports the following third-party identity providers:
diff --git a/docs/routes.json b/docs/routes.json
index 7bfa1ae323e..331f3ff693c 100644
--- a/docs/routes.json
+++ b/docs/routes.json
@@ -663,6 +663,10 @@
"title": "Directory sync",
"path": "/platform/account-management/directory-sync"
},
+ {
+ "title": "Multi-factor authentication",
+ "path": "/platform/account-management/mfa"
+ },
{
"title": "Delete an account",
"path": "/platform/account-management/delete-an-account"
diff --git a/package-lock.json b/package-lock.json
index 87768a52576..98584af26ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -37073,10 +37073,10 @@
},
"packages/liveblocks-client": {
"name": "@liveblocks/client",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/core": "3.3.4"
},
"devDependencies": {
"@liveblocks/eslint-config": "*",
@@ -37085,7 +37085,7 @@
},
"packages/liveblocks-core": {
"name": "@liveblocks/core",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"devDependencies": {
"@liveblocks/eslint-config": "*",
@@ -37103,11 +37103,11 @@
},
"packages/liveblocks-emails": {
"name": "@liveblocks/emails",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/core": "3.3.3",
- "@liveblocks/node": "3.3.3"
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/node": "3.3.4"
},
"devDependencies": {
"@liveblocks/eslint-config": "*",
@@ -37126,10 +37126,10 @@
},
"packages/liveblocks-node": {
"name": "@liveblocks/node",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/core": "3.3.3",
+ "@liveblocks/core": "3.3.4",
"@stablelib/base64": "^1.0.1",
"fast-sha256": "^1.3.0",
"node-fetch": "^2.6.1"
@@ -37144,11 +37144,11 @@
},
"packages/liveblocks-node-lexical": {
"name": "@liveblocks/node-lexical",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/core": "3.3.3",
- "@liveblocks/node": "3.3.3",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/node": "3.3.4",
"yjs": "^13.6.18"
},
"devDependencies": {
@@ -37165,11 +37165,11 @@
},
"packages/liveblocks-node-prosemirror": {
"name": "@liveblocks/node-prosemirror",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/core": "3.3.3",
- "@liveblocks/node": "3.3.3",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/node": "3.3.4",
"yjs": "^13.6.20"
},
"devDependencies": {
@@ -37189,11 +37189,11 @@
},
"packages/liveblocks-react": {
"name": "@liveblocks/react",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4"
},
"devDependencies": {
"@liveblocks/eslint-config": "*",
@@ -37223,15 +37223,15 @@
},
"packages/liveblocks-react-blocknote": {
"name": "@liveblocks/react-blocknote",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
- "@liveblocks/react-tiptap": "3.3.3",
- "@liveblocks/react-ui": "3.3.3",
- "@liveblocks/yjs": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
+ "@liveblocks/react-tiptap": "3.3.4",
+ "@liveblocks/react-ui": "3.3.4",
+ "@liveblocks/yjs": "3.3.4",
"@tiptap/core": "^2.7.2",
"vitest-tsconfig-paths": "^3.4.1"
},
@@ -37268,15 +37268,15 @@
},
"packages/liveblocks-react-lexical": {
"name": "@liveblocks/react-lexical",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/react-dom": "^2.1.1",
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
- "@liveblocks/react-ui": "3.3.3",
- "@liveblocks/yjs": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
+ "@liveblocks/react-ui": "3.3.4",
+ "@liveblocks/yjs": "3.3.4",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-toggle": "^1.1.0",
"yjs": "^13.6.18"
@@ -37805,15 +37805,15 @@
},
"packages/liveblocks-react-tiptap": {
"name": "@liveblocks/react-tiptap",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
- "@liveblocks/react-ui": "3.3.3",
- "@liveblocks/yjs": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
+ "@liveblocks/react-ui": "3.3.4",
+ "@liveblocks/yjs": "3.3.4",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-toggle": "^1.1.0",
"@tiptap/core": "^2.7.2",
@@ -38338,13 +38338,13 @@
},
"packages/liveblocks-react-ui": {
"name": "@liveblocks/react-ui",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-slot": "^1.1.0",
@@ -39672,11 +39672,11 @@
},
"packages/liveblocks-redux": {
"name": "@liveblocks/redux",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4"
},
"devDependencies": {
"@liveblocks/eslint-config": "*",
@@ -39833,10 +39833,10 @@
},
"packages/liveblocks-yjs": {
"name": "@liveblocks/yjs",
- "version": "3.3.3",
+ "version": "3.3.4",
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
"@noble/hashes": "^1.8.0",
"js-base64": "^3.7.7",
"y-indexeddb": "^9.0.12"
@@ -39902,11 +39902,11 @@
},
"packages/liveblocks-zustand": {
"name": "@liveblocks/zustand",
- "version": "3.3.3",
+ "version": "3.3.4",
"license": "Apache-2.0",
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4"
},
"devDependencies": {
"@liveblocks/eslint-config": "*",
diff --git a/packages/liveblocks-client/package.json b/packages/liveblocks-client/package.json
index a0ad8ebf8b2..7a8653e42bb 100644
--- a/packages/liveblocks-client/package.json
+++ b/packages/liveblocks-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/client",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A client that lets you interact with Liveblocks servers. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -34,7 +34,7 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/core": "3.3.4"
},
"devDependencies": {
"@liveblocks/eslint-config": "*",
diff --git a/packages/liveblocks-core/package.json b/packages/liveblocks-core/package.json
index c01bea4ff70..c9ac96302a4 100644
--- a/packages/liveblocks-core/package.json
+++ b/packages/liveblocks-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/core",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "Private internals for Liveblocks. DO NOT import directly from this package!",
"type": "module",
"main": "./dist/index.cjs",
diff --git a/packages/liveblocks-emails/package.json b/packages/liveblocks-emails/package.json
index 30471db7c93..c9717f5c90d 100644
--- a/packages/liveblocks-emails/package.json
+++ b/packages/liveblocks-emails/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/emails",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A set of functions and utilities to make sending emails based on Liveblocks notification events easy. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -35,8 +35,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.3.3",
- "@liveblocks/node": "3.3.3"
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/node": "3.3.4"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc"
diff --git a/packages/liveblocks-node-lexical/package.json b/packages/liveblocks-node-lexical/package.json
index cf839c57bb2..552e66f3c0e 100644
--- a/packages/liveblocks-node-lexical/package.json
+++ b/packages/liveblocks-node-lexical/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node-lexical",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A server-side utility that lets you modify lexical documents hosted in Liveblocks.",
"license": "Apache-2.0",
"type": "module",
@@ -34,8 +34,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.3.3",
- "@liveblocks/node": "3.3.3",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/node": "3.3.4",
"yjs": "^13.6.18"
},
"peerDependencies": {
diff --git a/packages/liveblocks-node-prosemirror/package.json b/packages/liveblocks-node-prosemirror/package.json
index 6f742a747e6..7f8431a6c61 100644
--- a/packages/liveblocks-node-prosemirror/package.json
+++ b/packages/liveblocks-node-prosemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node-prosemirror",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.",
"license": "Apache-2.0",
"type": "module",
@@ -34,8 +34,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.3.3",
- "@liveblocks/node": "3.3.3",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/node": "3.3.4",
"yjs": "^13.6.20"
},
"peerDependencies": {
diff --git a/packages/liveblocks-node/package.json b/packages/liveblocks-node/package.json
index fcaf239ded6..9f1292a245f 100644
--- a/packages/liveblocks-node/package.json
+++ b/packages/liveblocks-node/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A server-side utility that lets you set up a Liveblocks authentication endpoint. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -34,7 +34,7 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.3.3",
+ "@liveblocks/core": "3.3.4",
"@stablelib/base64": "^1.0.1",
"fast-sha256": "^1.3.0",
"node-fetch": "^2.6.1"
diff --git a/packages/liveblocks-react-blocknote/package.json b/packages/liveblocks-react-blocknote/package.json
index 6a91b65031c..9063d4c914a 100644
--- a/packages/liveblocks-react-blocknote/package.json
+++ b/packages/liveblocks-react-blocknote/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-blocknote",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "An integration of BlockNote + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"type": "module",
@@ -42,12 +42,12 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
- "@liveblocks/react-tiptap": "3.3.3",
- "@liveblocks/react-ui": "3.3.3",
- "@liveblocks/yjs": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
+ "@liveblocks/react-tiptap": "3.3.4",
+ "@liveblocks/react-ui": "3.3.4",
+ "@liveblocks/yjs": "3.3.4",
"@tiptap/core": "^2.7.2",
"vitest-tsconfig-paths": "^3.4.1"
},
diff --git a/packages/liveblocks-react-lexical/package.json b/packages/liveblocks-react-lexical/package.json
index 2ef7a461b01..d0f5000572e 100644
--- a/packages/liveblocks-react-lexical/package.json
+++ b/packages/liveblocks-react-lexical/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-lexical",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "An integration of Lexical + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"type": "module",
@@ -43,11 +43,11 @@
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.1",
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
- "@liveblocks/react-ui": "3.3.3",
- "@liveblocks/yjs": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
+ "@liveblocks/react-ui": "3.3.4",
+ "@liveblocks/yjs": "3.3.4",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-toggle": "^1.1.0",
"yjs": "^13.6.18"
diff --git a/packages/liveblocks-react-tiptap/package.json b/packages/liveblocks-react-tiptap/package.json
index 370627075ae..5a7a93710e0 100644
--- a/packages/liveblocks-react-tiptap/package.json
+++ b/packages/liveblocks-react-tiptap/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-tiptap",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "An integration of TipTap + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"type": "module",
@@ -43,11 +43,11 @@
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
- "@liveblocks/react-ui": "3.3.3",
- "@liveblocks/yjs": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
+ "@liveblocks/react-ui": "3.3.4",
+ "@liveblocks/yjs": "3.3.4",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-toggle": "^1.1.0",
"@tiptap/core": "^2.7.2",
diff --git a/packages/liveblocks-react-tiptap/src/LiveblocksExtension.ts b/packages/liveblocks-react-tiptap/src/LiveblocksExtension.ts
index 453ce20336d..3f30f2a549c 100644
--- a/packages/liveblocks-react-tiptap/src/LiveblocksExtension.ts
+++ b/packages/liveblocks-react-tiptap/src/LiveblocksExtension.ts
@@ -24,7 +24,11 @@ import type { Mark as PMMark } from "@tiptap/pm/model";
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { AiExtension } from "./ai/AiExtension";
-import { CommentsExtension } from "./comments/CommentsExtension";
+import {
+ areSetsEqual,
+ CommentsExtension,
+ FILTERED_THREADS_PLUGIN_KEY,
+} from "./comments/CommentsExtension";
import { MentionExtension } from "./mentions/MentionExtension";
import type {
LiveblocksExtensionOptions,
@@ -210,6 +214,37 @@ export const useLiveblocksExtension = (
useReportTextEditor(textEditorType, options.field ?? DEFAULT_OPTIONS.field);
+ const prevThreadsRef = useRef | undefined>(undefined);
+
+ useEffect(() => {
+ if (!isEditorReady) return;
+
+ if (!editor.current) return;
+
+ const newThreads = options.threads_experimental
+ ? new Set(options.threads_experimental.map((t) => t.id))
+ : undefined;
+
+ const hasFilteredThreadsChanged = !areSetsEqual(
+ prevThreadsRef.current,
+ newThreads
+ );
+
+ if (hasFilteredThreadsChanged) {
+ prevThreadsRef.current = newThreads;
+ }
+
+ if (hasFilteredThreadsChanged) {
+ editor.current.view.dispatch(
+ editor.current.state.tr.setMeta(FILTERED_THREADS_PLUGIN_KEY, {
+ filteredThreads: options.threads_experimental
+ ? new Set(options.threads_experimental.map((t) => t.id))
+ : undefined,
+ })
+ );
+ }
+ }, [isEditorReady, options.threads_experimental]);
+
const createTextMention = useCreateTextMention();
const deleteTextMention = useDeleteTextMention();
diff --git a/packages/liveblocks-react-tiptap/src/comments/CommentsExtension.ts b/packages/liveblocks-react-tiptap/src/comments/CommentsExtension.ts
index cc6ea97e494..3c36fa485b0 100644
--- a/packages/liveblocks-react-tiptap/src/comments/CommentsExtension.ts
+++ b/packages/liveblocks-react-tiptap/src/comments/CommentsExtension.ts
@@ -1,7 +1,7 @@
import { Extension, Mark, mergeAttributes } from "@tiptap/core";
import type { Node } from "@tiptap/pm/model";
import type { Transaction } from "@tiptap/pm/state";
-import { Plugin } from "@tiptap/pm/state";
+import { Plugin, PluginKey } from "@tiptap/pm/state";
import { Decoration, DecorationSet } from "@tiptap/pm/view";
import { ySyncPluginKey } from "y-prosemirror";
@@ -18,6 +18,10 @@ type ThreadPluginAction = {
data: string | null;
};
+export const FILTERED_THREADS_PLUGIN_KEY = new PluginKey<{
+ filteredThreads?: Set;
+}>();
+
/**
* Known issues: Overlapping marks are merged when reloading the doc. May be related:
* https://github.com/ueberdosis/tiptap/issues/4339
@@ -64,6 +68,22 @@ const Comment = Mark.create({
},
renderHTML({ HTMLAttributes }: { HTMLAttributes: Record }) {
+ const filteredThreads = this.editor
+ ? FILTERED_THREADS_PLUGIN_KEY.getState(this.editor.state)?.filteredThreads
+ : undefined;
+ const threadId = (HTMLAttributes as { ["data-lb-thread-id"]: string })[
+ "data-lb-thread-id"
+ ];
+ if (filteredThreads && !filteredThreads.has(threadId)) {
+ return [
+ "span",
+ mergeAttributes(HTMLAttributes, {
+ class: "lb-root lb-tiptap-thread-mark",
+ "data-hidden": "",
+ }),
+ ];
+ }
+
return [
"span",
mergeAttributes(HTMLAttributes, {
@@ -109,6 +129,17 @@ const Comment = Mark.create({
class: "lb-root lb-tiptap-thread-mark-selected",
})
);
+
+ const decoration = this.editor.view.dom.querySelector(
+ `.lb-tiptap-thread-mark[data-lb-thread-id="${thisThreadId}"]`
+ );
+
+ if (decoration) {
+ decoration.scrollIntoView({
+ behavior: "smooth",
+ block: "nearest",
+ });
+ }
}
}
});
@@ -192,6 +223,15 @@ const Comment = Mark.create({
return;
}
const threadId = commentMark?.attrs.threadId as string | undefined;
+
+ const filtered = FILTERED_THREADS_PLUGIN_KEY.getState(
+ view.state
+ )?.filteredThreads;
+ if (threadId && filtered && !filtered.has(threadId)) {
+ selectThread(null);
+ return;
+ }
+
selectThread(threadId ?? null);
},
},
@@ -201,7 +241,7 @@ const Comment = Mark.create({
});
export const CommentsExtension = Extension.create<
- never,
+ { filteredThreads?: Set },
CommentsExtensionStorage
>({
name: "liveblocksComments",
@@ -237,6 +277,19 @@ export const CommentsExtension = Extension.create<
return true;
},
selectThread: (id: string | null) => () => {
+ const filtered = FILTERED_THREADS_PLUGIN_KEY.getState(
+ this.editor.state
+ )?.filteredThreads;
+ if (id && filtered && !filtered.has(id)) {
+ this.editor.view.dispatch(
+ this.editor.state.tr.setMeta(THREADS_PLUGIN_KEY, {
+ name: ThreadPluginActions.SET_SELECTED_THREAD_ID,
+ data: null,
+ })
+ );
+ return true;
+ }
+
this.editor.view.dispatch(
this.editor.state.tr.setMeta(THREADS_PLUGIN_KEY, {
name: ThreadPluginActions.SET_SELECTED_THREAD_ID,
@@ -294,6 +347,85 @@ export const CommentsExtension = Extension.create<
},
},
}),
+ new Plugin({
+ key: FILTERED_THREADS_PLUGIN_KEY,
+ state: {
+ init: () => ({
+ filteredThreads: this.options.filteredThreads,
+ }),
+ apply(tr, value) {
+ const meta = tr.getMeta(FILTERED_THREADS_PLUGIN_KEY) as
+ | { filteredThreads?: Set }
+ | undefined;
+ if (meta?.filteredThreads) {
+ return { filteredThreads: meta.filteredThreads };
+ }
+ return value;
+ },
+ },
+ view: (view) => {
+ const syncDom = () => {
+ const filteredThreads = FILTERED_THREADS_PLUGIN_KEY.getState(
+ view.state
+ )?.filteredThreads;
+
+ // Toggle attribute for all comment-mark spans
+ const els = view.dom.querySelectorAll(
+ "span.lb-tiptap-thread-mark[data-lb-thread-id]"
+ );
+ els.forEach((el) => {
+ const id = el.getAttribute("data-lb-thread-id");
+ if (!id) return;
+ if (!filteredThreads || filteredThreads.has(id)) {
+ el.removeAttribute("data-hidden");
+ } else {
+ el.setAttribute("data-hidden", "");
+ }
+ });
+ };
+
+ queueMicrotask(syncDom);
+
+ return {
+ update: (view, prevState) => {
+ const curr = FILTERED_THREADS_PLUGIN_KEY.getState(
+ view.state
+ )?.filteredThreads;
+ const prev =
+ FILTERED_THREADS_PLUGIN_KEY.getState(
+ prevState
+ )?.filteredThreads;
+
+ if (
+ !areSetsEqual(prev, curr) ||
+ view.state.doc !== prevState.doc
+ ) {
+ syncDom();
+
+ const selected = THREADS_PLUGIN_KEY.getState(
+ view.state
+ )?.selectedThreadId;
+ if (selected && curr && !curr.has(selected)) {
+ view.dispatch(
+ view.state.tr.setMeta(THREADS_PLUGIN_KEY, {
+ name: ThreadPluginActions.SET_SELECTED_THREAD_ID,
+ data: null,
+ })
+ );
+ }
+ }
+ },
+ };
+ },
+ }),
];
},
});
+
+export function areSetsEqual(a?: Set, b?: Set): boolean {
+ if (a === b) return true;
+ if (!a || !b) return false;
+ if (a.size !== b.size) return false;
+ for (const v of a) if (!b.has(v)) return false;
+ return true;
+}
diff --git a/packages/liveblocks-react-tiptap/src/styles/index.css b/packages/liveblocks-react-tiptap/src/styles/index.css
index 0a8616a179e..7d6e8c13a3f 100644
--- a/packages/liveblocks-react-tiptap/src/styles/index.css
+++ b/packages/liveblocks-react-tiptap/src/styles/index.css
@@ -120,7 +120,7 @@
* Thread mark *
*************************************/
-:where(.lb-tiptap-thread-mark:not([data-orphan="true"])) {
+:where(.lb-tiptap-thread-mark:not([data-orphan="true"], [data-hidden])) {
background: var(--lb-accent-subtle);
color: var(--lb-foreground);
outline: none;
@@ -132,7 +132,7 @@
}
:where(
- .lb-tiptap-thread-mark:not([data-orphan="true"])
+ .lb-tiptap-thread-mark:not([data-orphan="true"], [data-hidden])
.lb-tiptap-thread-mark-selected
) {
color: var(--lb-accent);
@@ -141,6 +141,11 @@
text-underline-offset: 2px;
}
+.lb-tiptap-thread-mark {
+ /* Add some space above/below when scrolling to it. */
+ scroll-margin-block: var(--lb-spacing);
+}
+
/*************************************
* Anchored threads *
*************************************/
diff --git a/packages/liveblocks-react-tiptap/src/types.ts b/packages/liveblocks-react-tiptap/src/types.ts
index f5f4eb1f184..42c9ed7de93 100644
--- a/packages/liveblocks-react-tiptap/src/types.ts
+++ b/packages/liveblocks-react-tiptap/src/types.ts
@@ -3,6 +3,7 @@ import type {
ContextualPromptResponse,
Relax,
TextEditorType,
+ ThreadData,
} from "@liveblocks/core";
import type { LiveblocksYjsProvider } from "@liveblocks/yjs";
import type { Content, Range } from "@tiptap/core";
@@ -88,6 +89,7 @@ export type LiveblocksExtensionOptions = {
mentions?: boolean; // | MentionsConfiguration
ai?: boolean | AiConfiguration;
offlineSupport_experimental?: boolean;
+ threads_experimental?: ThreadData[];
initialContent?: Content;
enablePermanentUserData?: boolean;
/**
diff --git a/packages/liveblocks-react-ui/package.json b/packages/liveblocks-react-ui/package.json
index 4f7dd8c6874..91146008a06 100644
--- a/packages/liveblocks-react-ui/package.json
+++ b/packages/liveblocks-react-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-ui",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -76,9 +76,9 @@
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.2",
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
- "@liveblocks/react": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
+ "@liveblocks/react": "3.3.4",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-slot": "^1.1.0",
diff --git a/packages/liveblocks-react/package.json b/packages/liveblocks-react/package.json
index d9651dd9ff7..27695db5c45 100644
--- a/packages/liveblocks-react/package.json
+++ b/packages/liveblocks-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A set of React hooks and providers to use Liveblocks declaratively. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -61,8 +61,8 @@
"showdeps": "depcruise src --include-only '^src' --exclude='__tests__' --output-type dot | dot -T svg > /tmp/dependency-graph.svg && open /tmp/dependency-graph.svg"
},
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4"
},
"peerDependencies": {
"@types/react": "*",
diff --git a/packages/liveblocks-redux/package.json b/packages/liveblocks-redux/package.json
index eed10741dcc..048786b5984 100644
--- a/packages/liveblocks-redux/package.json
+++ b/packages/liveblocks-redux/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/redux",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A store enhancer to integrate Liveblocks into Redux stores. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -33,8 +33,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4"
},
"peerDependencies": {
"redux": "^4 || ^5"
diff --git a/packages/liveblocks-yjs/package.json b/packages/liveblocks-yjs/package.json
index 844abadefec..0b45e19ea46 100644
--- a/packages/liveblocks-yjs/package.json
+++ b/packages/liveblocks-yjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/yjs",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "Integrate your existing or new Yjs documents with Liveblocks.",
"icense": "Apache-2.0",
"type": "module",
@@ -33,8 +33,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3",
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4",
"@noble/hashes": "^1.8.0",
"js-base64": "^3.7.7",
"y-indexeddb": "^9.0.12"
diff --git a/packages/liveblocks-zustand/package.json b/packages/liveblocks-zustand/package.json
index cda5da7a59d..eede92bab50 100644
--- a/packages/liveblocks-zustand/package.json
+++ b/packages/liveblocks-zustand/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/zustand",
- "version": "3.3.3",
+ "version": "3.3.4",
"description": "A middleware for Zustand to automatically synchronize your stores with Liveblocks. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"type": "module",
@@ -34,8 +34,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/client": "3.3.3",
- "@liveblocks/core": "3.3.3"
+ "@liveblocks/client": "3.3.4",
+ "@liveblocks/core": "3.3.4"
},
"peerDependencies": {
"zustand": "^5.0.1"