From ca6d24796dbfbde1adda8113a37de21f7d62526f Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 29 Jun 2026 11:54:50 +0200 Subject: [PATCH 1/8] Release: 3.21 (#3542) Co-authored-by: Chris Nicholas --- CHANGELOG.md | 32 ++ .../pages/api-reference/liveblocks-client.mdx | 58 ++ docs/pages/api-reference/liveblocks-node.mdx | 37 +- .../api-reference/liveblocks-react-ui.mdx | 33 ++ docs/pages/api-reference/liveblocks-react.mdx | 34 +- docs/pages/authentication/permissions.mdx | 110 +++- .../comments/concepts.mdx | 28 +- .../comments/default-components.mdx | 1 + docs/references/v2.openapi.json | 44 +- .../pages/api/auth/access-token.ts | 22 +- e2e/next-sandbox/pages/comments/thread.tsx | 1 + .../pages/comments/visibility.tsx | 338 ++++++++++++ .../test/comments/visibility.test.ts | 498 ++++++++++++++++++ e2e/next-sandbox/test/utils.ts | 19 +- guides/guides.json | 14 + ...-to-add-private-commenting-to-your-app.mdx | 195 +++++++ ...to-filter-threads-using-query-language.mdx | 9 +- .../how-to-use-public-and-private-threads.mdx | 304 +++++++++++ .../src/__tests__/index.test.ts | 5 + .../src/__tests__/auth-manager.test.ts | 283 +++++++++- .../src/__tests__/permissions.test.ts | 206 +++++--- packages/liveblocks-core/src/api-client.ts | 73 ++- packages/liveblocks-core/src/auth-manager.ts | 13 +- packages/liveblocks-core/src/index.ts | 1 + packages/liveblocks-core/src/permissions.ts | 326 +++++++----- .../liveblocks-core/src/protocol/Comments.ts | 3 + packages/liveblocks-core/src/room.ts | 9 + .../src/types/LiveblocksError.ts | 7 +- .../src/__tests__/_helpers.ts | 1 + .../src/__tests__/Session.test.ts | 11 +- .../src/__tests__/client.test.ts | 8 +- packages/liveblocks-node/src/client.ts | 10 +- .../test-d/augmentation.test-d.ts | 1 + .../test-d/no-augmentation.test-d.ts | 1 + packages/liveblocks-python/README.md | 21 +- packages/liveblocks-python/README.mdx | 21 +- .../liveblocks-python/liveblocks/client.py | 70 +-- .../liveblocks/models/__init__.py | 4 + .../models/create_thread_request_body.py | 17 + .../create_thread_request_body_visibility.py | 6 + .../liveblocks/models/get_threads_response.py | 2 +- .../liveblocks/models/room_permission_item.py | 6 + .../liveblocks/models/thread.py | 12 +- .../liveblocks/models/thread_visibility.py | 6 + .../liveblocks-python/liveblocks/session.py | 12 + .../liveblocks-python/tests/test_session.py | 7 + .../src/__tests__/_liveblocks.config.ts | 2 +- .../src/__tests__/index.test.tsx | 30 +- .../src/components/Comment.tsx | 17 +- .../src/components/Composer.tsx | 46 +- .../src/components/Thread.tsx | 5 +- packages/liveblocks-react-ui/src/shared.ts | 20 +- .../test-d/augmentation.test-d.tsx | 34 +- .../test-d/no-augmentation.test-d.tsx | 60 ++- .../src/__tests__/ThreadDB.test.ts | 12 + .../src/__tests__/_dummies.ts | 1 + .../src/__tests__/_restMocks.ts | 2 + .../liveblocks-react/src/__tests__/_utils.tsx | 1 + .../src/__tests__/index.test.tsx | 65 +++ .../src/__tests__/umbrella-store/_dummies.ts | 2 + .../src/__tests__/useCreateThread.test.tsx | 5 + .../src/__tests__/useThreads.test.tsx | 19 +- .../src/__tests__/useUserThreads.test.tsx | 19 +- packages/liveblocks-react/src/lib/querying.ts | 1 + packages/liveblocks-react/src/room.tsx | 114 +++- packages/liveblocks-react/src/types/index.ts | 8 +- .../test-d/augmentation.test-d.tsx | 1 + .../test-d/no-augmentation.test-d.tsx | 1 + 68 files changed, 2961 insertions(+), 423 deletions(-) create mode 100644 e2e/next-sandbox/pages/comments/visibility.tsx create mode 100644 e2e/next-sandbox/test/comments/visibility.test.ts create mode 100644 guides/pages/how-to-add-private-commenting-to-your-app.mdx create mode 100644 guides/pages/how-to-use-public-and-private-threads.mdx create mode 100644 packages/liveblocks-python/liveblocks/models/create_thread_request_body_visibility.py create mode 100644 packages/liveblocks-python/liveblocks/models/thread_visibility.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b6dc1764c4b..98752e253e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ ## vNEXT (not yet released) +## v3.21.0 + +### All packages + +- Add support for public and private threads. Threads now have a `visibility` + property that is `"public"` by default but can be set to `"private"` when + created. Permissions can be used to decide which threads a user has access to, + and threads can also be queried by their visibility to create filtered views. +- Add scoped comments permissions such as `comments:public:write` and + `comments:private:none`. + +### `@liveblocks/client` + +- Add `visibility` to `createThread`. +- Support querying by `visibility` in `getThreads`. + +### `@liveblocks/react` + +- Add `visibility` to `useCreateThread`. +- Support querying by `visibility` in `useThreads`. + +### `@liveblocks/node` + +- Add `visibility` to `createThread`. +- Support querying by `visibility` in `getThreads`. + +### `@liveblocks/react-ui` + +- Add a `visibility` prop to `Composer`. +- Prevent `Composer` from collapsing after focusing and blurring unless it was + explicitly meant to support a collapsed state. + ## v3.20.1 ### `@liveblocks/client` diff --git a/docs/pages/api-reference/liveblocks-client.mdx b/docs/pages/api-reference/liveblocks-client.mdx index b69faf47bad..76879bd9a34 100644 --- a/docs/pages/api-reference/liveblocks-client.mdx +++ b/docs/pages/api-reference/liveblocks-client.mdx @@ -2914,6 +2914,11 @@ console.log(inboxNotifications); Only return `resolved` or `unresolved` threads. [Learn more](#filtering-resolved-status). + + Only return `public` or `private` threads. Permissions are taken into account so users + without access to private threads won’t receive them. [Learn + more](#filtering-visibility). + Only return `subscribed` or `unsubscribed` threads. [Learn more](#filtering-subscribed-status). @@ -2938,6 +2943,25 @@ const threads = await room.getThreads({ }); ``` +#### Filtering visibility [#filtering-visibility] + +You can filter threads based on visibility by passing `"public"` or `"private"` +to `query.visibility`. + +Permissions are taken into account, for example querying private threads returns +no threads if the current user does not have access to private threads. + +```ts +// Filtering for private threads +const threads = await room.getThreads({ + query: { + // +++ + visibility: "private", + // +++ + }, +}); +``` + #### Filtering subscribed status [#filtering-subscribed-status] You can filter threads by those that the user is subscribed to, or not, by @@ -3171,6 +3195,15 @@ const thread = await room.createThread({ Custom metadata to be attached to the thread, see [defining thread metadata](#defining-thread-metadata). + + Whether to create a public or private thread. Permissions are taken into + account so a user without write access to private threads can’t create a + private thread. + #### Creating thread content [#creating-thread-content] @@ -3245,6 +3278,31 @@ const metadata: Liveblocks["ThreadMetadata"] = { const thread = await room.createThread({ body, metadata }); ``` +#### Creating private threads [#creating-private-threads] + +Threads are public by default. To create a private thread, pass +`visibility: "private"`. + +```ts +const thread = await room.createThread({ + body, + // +++ + visibility: "private", + // +++ +}); +``` + +Permissions are taken into account when threads are created and retrieved. A +user without write access to private threads can’t create a private thread, and +users without read access to private threads won’t receive private threads from +[`room.getThreads`](#Room.getThreads) or [`room.getThread`](#Room.getThread). + + + +Private threads are only available on Team and Enterprise plans. + + + ### Room.deleteThread Deletes a thread by its ID. diff --git a/docs/pages/api-reference/liveblocks-node.mdx b/docs/pages/api-reference/liveblocks-node.mdx index 9db8c519565..74628f87f0a 100644 --- a/docs/pages/api-reference/liveblocks-node.mdx +++ b/docs/pages/api-reference/liveblocks-node.mdx @@ -49,8 +49,7 @@ const { body, status } = await liveblocks.identifyUser({ ``` - Learn how to - [get started with ID tokens](/docs/authentication#id-token). + Learn how to [get started with ID tokens](/docs/authentication#id-token). A number of options are also available, enabling you to set up permissions and @@ -91,10 +90,11 @@ expired. ##### Granting ID token permissions You can pass additional options to `identifyUser`, enabling you to create -complex [workspace permissions](/docs/authentication#id-token-workspace-permissions) and -[room permissions](/docs/authentication#id-token-room-permissions). For example, this -user can only see resources in the `acme-corp` workspace, and they’re part of a -`marketing` rooms group within it. +complex +[workspace permissions](/docs/authentication#id-token-workspace-permissions) and +[room permissions](/docs/authentication#id-token-room-permissions). For example, +this user can only see resources in the `acme-corp` workspace, and they’re part +of a `marketing` rooms group within it. ```ts const { body, status } = await liveblocks.identifyUser({ @@ -114,7 +114,8 @@ const { body, status } = await liveblocks.identifyUser({ ``` - Learn more about [ID token permissions](/docs/authentication#id-token-room-permissions). + Learn more about [ID token + permissions](/docs/authentication#id-token-room-permissions). ##### Text editor user data @@ -1893,9 +1894,9 @@ const { data: threads } = await liveblocks.getThreads({ console.log(threads); ``` -It’s also possible to filter threads by their string, boolean, and number -metadata using a query parameter. You can also pass `startsWith` to match the -start of a string. +It’s also possible to filter threads by visibility, resolved status, and their +string, boolean, and number metadata using a query parameter. You can also pass +`startsWith` to match the start of a string. ```ts const { data: threads } = await liveblocks.getThreads({ @@ -1905,6 +1906,10 @@ const { data: threads } = await liveblocks.getThreads({ query: { // Optional, filter based on resolved status resolved: false, + + // Optional, filter based on visibility + visibility: "private", + // Optional, filter for metadata values metadata: { status: "open", @@ -1926,8 +1931,9 @@ instead of a `query` object. #### Liveblocks.createThread [#post-rooms-roomId-threads] -Creates a new thread within a specific room, using room ID and thread data. This -is a wrapper around the +Creates a new thread within a specific room, using room ID and thread data. +Threads are public by default, but can be created as private by passing +`visibility: "private"`. This is a wrapper around the [Create Thread API](/docs/api-reference/rest-api-endpoints#post-rooms-roomId-threads) and returns the new thread. @@ -1991,8 +1997,8 @@ You can also convert a Markdown string to a `CommentBody` with -This method has a number of options, allowing for custom metadata and a creation -date for the comment. +This method has a number of options, allowing for custom metadata, thread +visibility, and a creation date for the comment. ```ts const thread = await liveblocks.createThread({ @@ -2006,6 +2012,9 @@ const thread = await liveblocks.createThread({ pinned: true, }, + // Optional, defaults to "public" + visibility: "private", + // Data for the first comment in the thread comment: { // The ID of the user that created the comment diff --git a/docs/pages/api-reference/liveblocks-react-ui.mdx b/docs/pages/api-reference/liveblocks-react-ui.mdx index 63f6916796c..71a46f9c2e4 100644 --- a/docs/pages/api-reference/liveblocks-react-ui.mdx +++ b/docs/pages/api-reference/liveblocks-react-ui.mdx @@ -1464,6 +1464,31 @@ declare global { } ``` +##### Creating private threads + +Threads are public by default. If you’d like the composer to create private +threads, you can add a `visibility` prop. + +```tsx +import { Composer } from "@liveblocks/react-ui"; + +// Creates a new private thread +function Component() { + return ; +} +``` + +Permissions are taken into account when threads are created and retrieved. A +user without write access to private threads can’t create a private thread, and +users without read access to private threads won’t receive private threads from +[`useThreads`](/docs/api-reference/liveblocks-react#useThreads). + + + +Private threads are only available on Team and Enterprise plans. + + + ##### Replying to a thread If you provide a `threadId`, then submitting the composer will add a new reply @@ -1562,6 +1587,14 @@ Learn more about mutation hooks under The metadata of the thread to create. + + Whether to create a public or private thread. Only applies when creating a + new thread, and requires write access to the selected visibility. + { // Can happen if you use Comments or Notifications case "CREATE_THREAD_ERROR": - const { roomId, threadId, commentId, body, metadata } = error.context; + const { roomId, threadId, commentId, body, visibility, metadata } = + error.context; break; case "DELETE_THREAD_ERROR": @@ -4074,8 +4075,9 @@ function Component() { Optional configuration object. - Optional query to filter threads by resolved status and metadata values. - [Learn more](/docs/api-reference/liveblocks-react#useThreads-query). + Optional query to filter threads by visibility, resolved status, subscribed + status, and metadata values. [Learn + more](/docs/api-reference/liveblocks-react#useThreads-query). Whether to scroll to a comment if the URL's hash is set to a comment ID. @@ -4116,11 +4118,13 @@ function Component() { #### Querying threads [#useThreads-query] It’s possible to return threads that match a certain query with the `query` -option. You can filter threads based on their resolved status, if the user is -subscribed to them, and metadata. Additionally, you can filter for metadata -strings that being with certain characters using `startsWith` and you can filter -for metadata numbers using `gt`, `lt`, `gte`, and `lte`. Returned threads match -the entire query. +option. You can filter threads based on their visibility, resolved status, if +the user is subscribed to them, and metadata. Additionally, you can filter for +metadata strings that begin with certain characters using `startsWith` and you +can filter for metadata numbers using `gt`, `lt`, `gte`, and `lte`. Returned +threads match the entire query. Permissions are taken into account so querying +private threads will return no private threads if the current user does not have +access to them. ```tsx // Returns threads that match the entire `query`, e.g. { color: "blue", pinned: true, ... } @@ -4129,6 +4133,9 @@ const { threads } = useThreads({ // Filter for unresolved threads resolved: false, + // Filter for private threads + visibility: "private", + // Filter for threads that the user is subscribed to subscribed: true, @@ -4280,7 +4287,9 @@ const { threads } = useThreads({ scrollOnLoad: false }); ### useCreateThread [@badge=RoomProvider] Returns a function that optimistically creates a thread with an initial comment, -and optionally some thread and comment metadata. +and optionally some thread metadata, comment metadata, and visibility. Threads +are public by default. Permissions are taken into account so a user without +write access to private threads can’t create a private one. ```tsx import { useCreateThread } from "@liveblocks/react/suspense"; @@ -4291,6 +4300,7 @@ const thread = createThread({ attachments: [], metadata: {}, commentMetadata: {}, + visibility: "private", }); ``` @@ -4300,7 +4310,8 @@ const thread = createThread({ type="(options: CreateThreadOptions) => ThreadData" > A function that creates a thread with an initial comment, and optionally - thread and comment metadata. Returns the optimistic thread object. + thread metadata, comment metadata, and visibility. Returns the optimistic + thread object. @@ -4316,7 +4327,8 @@ import { useErrorListener } from "@liveblocks/react/suspense"; useErrorListener((error) => { if (error.context.type === "CREATE_THREAD_ERROR") { - const { roomId, threadId, commentId, body, metadata } = error.context; + const { roomId, threadId, commentId, body, visibility, metadata } = + error.context; console.log(`Problem creating thread ${threadId}`); } }); diff --git a/docs/pages/authentication/permissions.mdx b/docs/pages/authentication/permissions.mdx index e0d723cdb0e..e29d98b00bd 100644 --- a/docs/pages/authentication/permissions.mdx +++ b/docs/pages/authentication/permissions.mdx @@ -6,14 +6,67 @@ meta: --- Permissions define what an authenticated user can do with Liveblocks resources -such as rooms, comments, and feeds. +such as rooms, comments, and feeds. When using +[ID token authentication](/docs/authentication), permissions are set on rooms. +With [access tokens](/docs/authentication/access-token), you permissions are +granted when a user authenticates. + + + +| Permission | Resource | Description | +| ------------------------ | --------------- | ----------------------------------------------------- | +| **`*:read`** | | **Read access to everything.** | +| **`*:write`** | | **Write access to everything.** | +| `storage:read` | Storage | Read access to storage (Liveblocks Storage and Yjs). | +| `storage:write` | Storage | Write access to storage (Liveblocks Storage and Yjs). | +| `storage:none` | Storage | No access to storage (Liveblocks Storage and Yjs). | +| `comments:read` | Comments | Read access to public and private threads. | +| `comments:write` | Comments | Write access to public and private threads. | +| `comments:none` | Comments | No access to public and private threads. | +| `comments:public:read` | Public threads | Read access to public threads. | +| `comments:public:write` | Public threads | Write access to public threads. | +| `comments:public:none` | Public threads | No access to public threads. | +| `comments:private:read` | Private threads | Read access to private threads. | +| `comments:private:write` | Private threads | Write access to private threads. | +| `comments:private:none` | Private threads | No access to private threads. | +| `feeds:read` | Feeds | Read access to feeds. | +| `feeds:write` | Feeds | Write access to feeds. | +| `feeds:none` | Feeds | No access to feeds. | -With [ID tokens](/docs/authentication#id-token), permissions live on the room -and Liveblocks checks them when a user connects. With -[access tokens](/docs/authentication/access-token), you grant permissions when -you prepare a session. +
+ +## Example usage + +Permissions are set as an array of strings, in any order, and setting an empty +array means no access. You can use these permissions with Liveblocks APIs, for +example when creating a room. -## Permission format [#permission-format] +```ts +const room = await liveblocks.createRoom("my-room-id", { + // By default, nobody has access to the room + // +++ + defaultAccesses: [], + // +++ + + // `groupIds: ["viewer"]` users are read-only, but can leave comments + groupsAccesses: { + // +++ + viewer: ["*:read", "comments:write"], + // +++ + }, + + // `userId: "marc"` has full write access + usersAccesses: { + // +++ + "marc@example.com": ["*:write"], + // +++ + }, +}); +``` + +Note that `groupIds` and `userId` are set when authenticating a user. + +## More information [#permission-format] A user’s access to a room is defined by a list of permissions. @@ -43,8 +96,15 @@ You can opt into or opt out of access to specific room resources: - **Storage** with `storage:read`, `storage:write`, or `storage:none`. - **Comments** with `comments:read`, `comments:write`, or `comments:none`. +- **Public threads** with `comments:public:read`, `comments:public:write`, or + `comments:public:none`. +- **Private threads** with `comments:private:read`, `comments:private:write`, or + `comments:private:none`. - **Feeds** with `feeds:read`, `feeds:write`, or `feeds:none`. +The `comments:*` permissions apply to public and private threads. Use +`comments:public:*` and `comments:private:*` to override one visibility. + Here’s an example giving write access to everything except read-only access to storage: @@ -66,25 +126,31 @@ comments and no access to feeds: ]; ``` -### List of all permissions +Here’s an example giving write access to public threads, but no access to +private threads: + +```ts +[ + "*:write", + "comments:private:none", // Remove access to private threads +]; +``` + +### Public and private threads [#public-private-threads] - +By default, threads are visible to all users that have permission to read the +room, but they can be marked as private by passing `visibility: "private"` when +the thread is created. Users then need private comments permissions to create or +read private threads. For more information, read +[how to add private commenting to your app](/docs/guides/how-to-add-private-commenting-to-your-app), +or find a summary of the APIs under +[public and private threads](/docs/guides/how-to-use-public-and-private-threads). -| Permission | Resource | Description | -| ---------------- | -------- | ----------------------------------------------------- | -| **`*:read`** | | **Read access to everything.** | -| **`*:write`** | | **Write access to everything.** | -| `storage:read` | Storage | Read access to storage (Liveblocks Storage and Yjs). | -| `storage:write` | Storage | Write access to storage (Liveblocks Storage and Yjs). | -| `storage:none` | Storage | No access to storage (Liveblocks Storage and Yjs). | -| `comments:read` | Comments | Read access to comments. | -| `comments:write` | Comments | Write access to comments. | -| `comments:none` | Comments | No access to comments. | -| `feeds:read` | Feeds | Read access to feeds. | -| `feeds:write` | Feeds | Write access to feeds. | -| `feeds:none` | Feeds | No access to feeds. | + -
+Private threads are only available on Team and Enterprise plans. + + ## Where to use permissions [#where-to-use-permissions] diff --git a/docs/pages/collaboration-features/comments/concepts.mdx b/docs/pages/collaboration-features/comments/concepts.mdx index 4c404955d5a..fd0cff95a7f 100644 --- a/docs/pages/collaboration-features/comments/concepts.mdx +++ b/docs/pages/collaboration-features/comments/concepts.mdx @@ -37,6 +37,7 @@ Here’s an example of a thread object. roomId: "my-room-id", createdAt: Date , resolved: false, + visibility: "public", comments: [ // A list of comments in the thread // ... @@ -71,14 +72,16 @@ a thread. The first comment in a thread is displayed at the top. Here’s an example of a single comment inside a thread object. -```ts highlight="8-24" +```ts { type: "thread", id: "th_sf8s6sh...", roomId: "my-room-id", createdAt: Date , resolved: false, + visibility: "public", comments: [ + // +++ { type: "comment", threadId: "th_sf8s6sh...", @@ -96,6 +99,7 @@ Here’s an example of a single comment inside a thread object. // ... }, }, + // +++ // Other comments in the thread // ... @@ -142,3 +146,25 @@ hidden completely. A thread is only deleted after all its comments have been deleted. + +### Private comments + +Each thread has a `visibility` that is either `"public"` or `"private"`, and +threads are public by default. Public and private threads work in the same way, +but when authenticating users, you can select whether the user should have +access to public or private threads. + +Using the visibility option allows you to +[add private commenting to your app](/docs/guides/how-to-add-private-commenting-to-your-app), +enabling different two different tiers of commenting permissions in one room. +This is particularly useful for creating internal or team-only discussions, +whilst still allowing public comments for other users. + +For a summary of the APIs, read +[how to use public and private threads](/docs/guides/how-to-use-public-and-private-threads). + + + +Private threads are only available on Team and Enterprise plans. + + diff --git a/docs/pages/collaboration-features/comments/default-components.mdx b/docs/pages/collaboration-features/comments/default-components.mdx index f6940edcb20..94ac0424312 100644 --- a/docs/pages/collaboration-features/comments/default-components.mdx +++ b/docs/pages/collaboration-features/comments/default-components.mdx @@ -86,6 +86,7 @@ function Component() { [`Composer`][] can also be used in other ways: - [Adding metadata to a new thread](/docs/api-reference/liveblocks-react-ui#Adding-thread-metadata) +- [Creating private threads](/docs/api-reference/liveblocks-react-ui#Creating-private-threads) - [Replying to a thread](/docs/api-reference/liveblocks-react-ui#Replying-to-a-thread) - [Adding metadata to a reply](/docs/api-reference/liveblocks-react-ui#Adding-comment-metadata) - [Modifying a comment](/docs/api-reference/liveblocks-react-ui#Modifying-a-comment) diff --git a/docs/references/v2.openapi.json b/docs/references/v2.openapi.json index 5a698cb9582..319111ae54d 100644 --- a/docs/references/v2.openapi.json +++ b/docs/references/v2.openapi.json @@ -126,7 +126,7 @@ }, "post": { "summary": "Create room", - "description": "This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided. \n- `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` are optional fields.\n", + "description": "This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided. \n- `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, `[\"*:write\"]`, or a more granular permission list. \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` contains group-specific permission lists and is optional.\n", "tags": ["Room"], "parameters": [ { @@ -342,7 +342,7 @@ } }, "operationId": "update-room", - "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` could be `[]` or `[\"*:write\"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.", + "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.updateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomid). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, `[\"*:write\"]`, or a more granular permission list.\n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` contains group-specific permission lists and is optional.", "requestBody": { "required": true, "content": { @@ -575,7 +575,7 @@ } }, "operationId": "upsert-room", - "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-roomId). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). \n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` could be `[]` or `[\"*:write\"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field.", + "description": "This endpoint updates specific properties of a room. Corresponds to [`liveblocks.upsertRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#upsert-rooms-roomId). \n\nIt’s not necessary to provide the entire room’s information. \nSetting a property to `null` means to delete this property. For example, if you want to remove access to a specific user without losing other users: \n``{\n \"usersAccesses\": {\n \"john\": null\n }\n}``\n`defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated.\n\n- `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, `[\"*:write\"]`, or a more granular permission list.\n- `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field.\n- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field.\n- `groupsAccesses` contains group-specific permission lists and is optional.", "requestBody": { "required": true, "content": { @@ -1691,7 +1691,7 @@ { "schema": { "type": "string", - "description": "Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata[\"status\"]:\"open\" AND metadata[\"color\"]:\"red\" AND resolved:true`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language).", + "description": "Query to filter threads. You can filter by `metadata`, `resolved`, and `visibility`, for example, `metadata[\"status\"]:\"open\" AND metadata[\"color\"]:\"red\" AND resolved:true AND visibility:\"private\"`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language).", "example": "metadata[\"color\"]:\"blue\"" }, "in": "query", @@ -1733,6 +1733,8 @@ ], "createdAt": "2019-08-24T14:15:22Z", "metadata": {}, + "resolved": false, + "visibility": "public", "updatedAt": "2019-08-24T14:15:22Z" } ] @@ -1799,9 +1801,12 @@ } ], "createdAt": "2022-07-13T14:32:50.697Z", + "updatedAt": "2022-07-13T14:32:50.697Z", "metadata": { "color": "blue" - } + }, + "resolved": false, + "visibility": "public" } } } @@ -1911,6 +1916,8 @@ ], "createdAt": "2019-08-24T14:15:22Z", "metadata": {}, + "resolved": false, + "visibility": "public", "updatedAt": "2019-08-24T14:15:22Z" } } @@ -7197,6 +7204,12 @@ "comments:read", "comments:write", "comments:none", + "comments:public:read", + "comments:public:write", + "comments:public:none", + "comments:private:read", + "comments:private:write", + "comments:private:none", "feeds:read", "feeds:write", "feeds:none" @@ -8070,7 +8083,8 @@ "createdAt": "2022-07-13T14:32:50.697Z", "updatedAt": "2022-07-13T14:32:50.697Z", "metadata": {}, - "resolved": false + "resolved": false, + "visibility": "public" } ] } @@ -8104,6 +8118,10 @@ "resolved": { "type": "boolean" }, + "visibility": { + "type": "string", + "enum": ["public", "private"] + }, "updatedAt": { "type": "string", "format": "date-time" @@ -8117,7 +8135,8 @@ "createdAt", "updatedAt", "metadata", - "resolved" + "resolved", + "visibility" ], "example": { "type": "thread", @@ -8145,7 +8164,8 @@ "metadata": { "color": "blue" }, - "resolved": false + "resolved": false, + "visibility": "public" } }, "Feed": { @@ -8365,6 +8385,11 @@ }, "metadata": { "$ref": "#/components/schemas/ThreadMetadata" + }, + "visibility": { + "type": "string", + "enum": ["public", "private"], + "default": "public" } }, "required": ["comment"], @@ -8384,7 +8409,8 @@ }, "metadata": { "color": "blue" - } + }, + "visibility": "private" } ] }, diff --git a/e2e/next-sandbox/pages/api/auth/access-token.ts b/e2e/next-sandbox/pages/api/auth/access-token.ts index ecb8d04a36e..2a064531e90 100644 --- a/e2e/next-sandbox/pages/api/auth/access-token.ts +++ b/e2e/next-sandbox/pages/api/auth/access-token.ts @@ -1,4 +1,4 @@ -import { nn } from "@liveblocks/core"; +import { nn, type Permission as PermissionToken } from "@liveblocks/core"; import { Liveblocks } from "@liveblocks/node"; import type { NextApiRequest, NextApiResponse } from "next"; @@ -17,6 +17,22 @@ const liveblocks = new Liveblocks({ ), }); +type QueryValue = string | string[] | undefined; + +function getQueryValues(value: QueryValue) { + if (value === undefined) { + return undefined; + } + + return Array.isArray(value) ? value : [value]; +} + +function getPermissions(value: QueryValue) { + const scopes = getQueryValues(value); + + return scopes as PermissionToken[] | undefined; +} + export default async function accessTokenAuth( req: NextApiRequest, res: NextApiResponse @@ -35,7 +51,9 @@ export default async function accessTokenAuth( }, } ); - session.allow("e2e*", session.FULL_ACCESS); + + const permissions = getPermissions(req.query.permissions); + session.allow("e2e*", permissions ?? session.FULL_ACCESS); const response = await session.authorize(); res.status(response.status).end(response.body); } diff --git a/e2e/next-sandbox/pages/comments/thread.tsx b/e2e/next-sandbox/pages/comments/thread.tsx index 34fbf176884..a0163cf0c57 100644 --- a/e2e/next-sandbox/pages/comments/thread.tsx +++ b/e2e/next-sandbox/pages/comments/thread.tsx @@ -39,6 +39,7 @@ function Sandbox() { createdAt: date, updatedAt: date, resolved: false, + visibility: "public", metadata: {}, comments: [ { diff --git a/e2e/next-sandbox/pages/comments/visibility.tsx b/e2e/next-sandbox/pages/comments/visibility.tsx new file mode 100644 index 00000000000..47db1036019 --- /dev/null +++ b/e2e/next-sandbox/pages/comments/visibility.tsx @@ -0,0 +1,338 @@ +import type { + BaseMetadata, + BaseUserMeta, + Json, + JsonObject, + ThreadVisibility, +} from "@liveblocks/core"; +import { + createRoomContext, + useErrorListener, + useSyncStatus, +} from "@liveblocks/react"; +import { useEffect, useMemo, useState } from "react"; + +import { getRoomFromUrl, Row } from "../../utils"; +import Button from "../../utils/Button"; +import { createLiveblocksClient } from "../../utils/createClient"; + +const E2E_CASE = "visibility-threads"; + +type VisibilityFilter = ThreadVisibility | "all"; +type PageMode = "read" | "create"; + +const client = createLiveblocksClient({ + authEndpoint: async () => { + const params = new URLSearchParams(); + const user = getOptionalUrlParam("user"); + const permissions = getUrlParamValues("permissions"); + + if (user !== undefined) { + params.set("user", user); + } + + for (const permission of permissions) { + params.append("permissions", permission); + } + + const query = params.toString(); + const response = await fetch( + query ? `/api/auth/access-token?${query}` : "/api/auth/access-token" + ); + + return response.json(); + }, +}); + +const { RoomProvider, useThreads, useCreateThread } = createRoomContext< + JsonObject, + never, + BaseUserMeta, + Json, + BaseMetadata +>(client); + +export default function Home() { + const roomId = getRoomFromUrl(); + + return ( + + + + ); +} + +function Sandbox() { + const params = usePageParams(); + + if (params === undefined) { + return null; + } + + if (params.mode === "create") { + return ; + } + + return ; +} + +function ReadSandbox({ + runId, + visibility, +}: { + runId: string; + visibility: VisibilityFilter; +}) { + const createThread = useCreateThread(); + const syncStatus = useSyncStatus({ smooth: true }); + const isSynced = syncStatus === "synchronized"; + + const metadata = useMemo( + () => ({ + e2eCase: E2E_CASE, + e2eRun: runId, + }), + [runId] + ); + + const query = useMemo( + () => (visibility === "all" ? undefined : { visibility }), + [visibility] + ); + + const result = useThreads(query === undefined ? {} : { query }); + const threads = result.threads ?? []; + const publicThreads = threads.filter( + (thread) => thread.visibility === "public" + ); + const privateThreads = threads.filter( + (thread) => thread.visibility === "private" + ); + const threadVisibilities = threads + .map((thread) => thread.visibility) + .sort(); + const error = "error" in result ? result.error?.message : undefined; + + return ( + <> + + + + + + + + + + + + +
+ + {visibility === "all" ? null : ( + + )} + + ); +} + +function CreateSandbox({ + runId, + visibility, +}: { + runId: string; + visibility: VisibilityFilter; +}) { + const createThread = useCreateThread(); + const syncStatus = useSyncStatus({ smooth: true }); + const [error, setError] = useState< + | { + message: string; + cause: string | undefined; + contextType: string | undefined; + } + | undefined + >(); + + const metadata = useMemo( + () => ({ + e2eCase: E2E_CASE, + e2eRun: runId, + }), + [runId] + ); + + useErrorListener((err) => { + setError({ + message: err.message, + cause: err.cause instanceof Error ? err.cause.message : undefined, + contextType: err.context.type, + }); + }); + + return ( + <> + + + + + + + + +
+ + {visibility === "all" ? null : ( + + )} + + ); +} + +function usePageParams() { + const [params, setParams] = useState< + | { + runId: string; + visibility: VisibilityFilter; + mode: PageMode; + } + | undefined + >(); + + useEffect(() => { + setParams({ + runId: getRunIdFromUrl(), + visibility: getVisibilityFilterFromUrl(), + mode: getPageModeFromUrl(), + }); + }, []); + + return params; +} + +function getPageModeFromUrl(): PageMode { + if (typeof window === "undefined") { + return "read"; + } + + const mode = getOptionalUrlParam("mode"); + if (mode === undefined || mode === "read") { + return "read"; + } + + if (mode === "create") { + return mode; + } + + throw new Error("Specify ?mode=read or ?mode=create in URL"); +} + +function getOptionalUrlParam(name: string): string | undefined { + if (typeof window === "undefined") { + return undefined; + } + + return new URL(window.location.href).searchParams.get(name) ?? undefined; +} + +function getUrlParamValues(name: string): string[] { + if (typeof window === "undefined") { + return []; + } + + return new URL(window.location.href).searchParams.getAll(name); +} + +function getRunIdFromUrl(): string { + if (typeof window === "undefined") { + return "run-id-placeholder-for-ssr"; + } + + const runId = getOptionalUrlParam("run"); + if (runId === undefined) { + throw new Error("Specify ?run= in URL, please"); + } + + return runId; +} + +function getVisibilityFilterFromUrl(): VisibilityFilter { + if (typeof window === "undefined") { + return "all"; + } + + const visibility = getOptionalUrlParam("visibility"); + if (visibility === "all") { + return visibility; + } + + if (visibility === "public" || visibility === "private") { + return visibility; + } + + throw new Error( + "Specify ?visibility=all, ?visibility=public, or ?visibility=private in URL" + ); +} diff --git a/e2e/next-sandbox/test/comments/visibility.test.ts b/e2e/next-sandbox/test/comments/visibility.test.ts new file mode 100644 index 00000000000..9aa14701b90 --- /dev/null +++ b/e2e/next-sandbox/test/comments/visibility.test.ts @@ -0,0 +1,498 @@ +import type { Page, TestInfo } from "@playwright/test"; +import { expect, test } from "@playwright/test"; + +import { genRoomId, preparePage, waitForJson } from "../utils"; + +const TEST_URL = "http://localhost:3007/comments/visibility"; +const SLOW = { timeout: 20_000 }; +const VISIBILITY_PERMISSIONS = { + public: ["*:read", "comments:none", "comments:public:write"], + private: ["*:read", "comments:none", "comments:private:write"], +} satisfies Record<"public" | "private", readonly string[]>; +const PUBLIC_COMMENTS_WRITE_PRIVATE_NONE_PERMISSIONS: readonly string[] = [ + "*:read", + "comments:write", + "comments:private:none", +]; +const BASE_WRITE_PERMISSIONS: readonly string[] = ["*:write"]; +const BROAD_COMMENTS_WRITE_PERMISSIONS: readonly string[] = [ + "*:read", + "comments:write", +]; + +test.describe("Thread visibility", () => { + let pages: Page[] = []; + + test.afterEach(async () => { + await Promise.all( + pages + .filter((page) => !page.isClosed()) + .map((page) => page.close()) + ); + pages = []; + }); + + test( + "creates and fetches public and private thread visibility", + async ({}, testInfo) => { + const run = [ + Date.now().toString(36), + testInfo.workerIndex, + testInfo.retry, + Math.random().toString(16).slice(2, 8), + ].join("-"); + const room = getRoomId(testInfo, run, "all"); + + const publicPage = await openPage({ + room, + run, + user: 1, + visibility: "public", + x: 0, + }); + + await waitForPageLoaded(publicPage); + + const publicThreadResponse$ = waitForCreateThreadResponse(publicPage); + await publicPage.click("#create-thread"); + await publicThreadResponse$; + await publicPage.close(); + + const privatePage = await openPage({ + room, + run, + user: 2, + visibility: "private", + x: 640, + }); + + await waitForPageLoaded(privatePage); + + const privateThreadResponse$ = waitForCreateThreadResponse(privatePage); + await privatePage.click("#create-thread"); + await privateThreadResponse$; + await privatePage.close(); + + const verifierPage = await openPage({ + room, + run, + user: 1, + visibility: "all", + x: 0, + }); + + await waitForPageLoaded(verifierPage); + await waitForJson(verifierPage, "#threadCount", 2, SLOW); + await waitForJson(verifierPage, "#publicThreadCount", 1, SLOW); + await waitForJson(verifierPage, "#privateThreadCount", 1, SLOW); + await waitForJson( + verifierPage, + "#threadVisibilities", + ["private", "public"], + SLOW + ); + await verifierPage.close(); + } + ); + + test( + "creates threads with matching visibility-specific permissions", + async ({}, testInfo) => { + const run = [ + Date.now().toString(36), + testInfo.workerIndex, + testInfo.retry, + Math.random().toString(16).slice(2, 8), + ].join("-"); + const publicRoom = getRoomId(testInfo, run, "public"); + const privateRoom = getRoomId(testInfo, run, "private"); + + await createThreadWithVisibilityPermissions({ + room: publicRoom, + run, + user: 1, + visibility: "public", + x: 0, + }); + await verifyPersistedVisibility({ + room: publicRoom, + run, + user: 1, + visibility: "public", + x: 0, + }); + + await createThreadWithVisibilityPermissions({ + room: privateRoom, + run, + user: 2, + visibility: "private", + x: 640, + }); + await verifyPersistedVisibility({ + room: privateRoom, + run, + user: 2, + visibility: "private", + x: 640, + }); + } + ); + + test( + "inherits base and broad comments permissions for thread visibility", + async ({}, testInfo) => { + const run = [ + Date.now().toString(36), + testInfo.workerIndex, + testInfo.retry, + Math.random().toString(16).slice(2, 8), + ].join("-"); + const cases = [ + { + id: "base-write-public", + user: 1, + visibility: "public", + permissions: BASE_WRITE_PERMISSIONS, + x: 0, + }, + { + id: "base-write-private", + user: 1, + visibility: "private", + permissions: BASE_WRITE_PERMISSIONS, + x: 640, + }, + { + id: "comments-write-public", + user: 2, + visibility: "public", + permissions: BROAD_COMMENTS_WRITE_PERMISSIONS, + x: 0, + }, + { + id: "comments-write-private", + user: 2, + visibility: "private", + permissions: BROAD_COMMENTS_WRITE_PERMISSIONS, + x: 640, + }, + ] satisfies Array<{ + id: string; + user: number; + visibility: "public" | "private"; + permissions: readonly string[]; + x: number; + }>; + + for (const testCase of cases) { + const room = getRoomId(testInfo, run, testCase.id); + + await createThreadWithExplicitPermissions({ + room, + run, + user: testCase.user, + visibility: testCase.visibility, + permissions: testCase.permissions, + x: testCase.x, + }); + await verifyPersistedVisibility({ + room, + run, + user: testCase.user, + visibility: testCase.visibility, + x: testCase.x, + }); + } + } + ); + + test( + "rejects threads created with mismatched visibility-specific permissions", + async ({}, testInfo) => { + const run = [ + Date.now().toString(36), + testInfo.workerIndex, + testInfo.retry, + Math.random().toString(16).slice(2, 8), + ].join("-"); + const room = getRoomId(testInfo, run, "mismatch"); + + const page = await openPage({ + room, + run, + user: 1, + visibility: "private", + permissions: VISIBILITY_PERMISSIONS.public, + mode: "create", + x: 0, + }); + + await waitForCreatePageLoaded(page); + await page.click("#create-thread"); + await waitForJson(page, "#errorContextType", "CREATE_THREAD_ERROR", SLOW); + await expect(page.locator("#errorCause")).toContainText( + /forbidden|permission|unauthorized|not allowed|403/i, + SLOW + ); + await page.close(); + + const verifierPage = await openPage({ + room, + run, + user: 1, + visibility: "all", + x: 0, + }); + + await waitForPageLoaded(verifierPage); + await waitForJson(verifierPage, "#threadCount", 0, SLOW); + await verifierPage.close(); + } + ); + + test( + "applies explicit permission tokens with a private visibility override", + async ({}, testInfo) => { + const run = [ + Date.now().toString(36), + testInfo.workerIndex, + testInfo.retry, + Math.random().toString(16).slice(2, 8), + ].join("-"); + const publicRoom = getRoomId(testInfo, run, "public"); + const privateRoom = getRoomId(testInfo, run, "private"); + + await createThreadWithExplicitPermissions({ + room: publicRoom, + run, + user: 1, + visibility: "public", + permissions: PUBLIC_COMMENTS_WRITE_PRIVATE_NONE_PERMISSIONS, + x: 0, + }); + await verifyPersistedVisibility({ + room: publicRoom, + run, + user: 1, + visibility: "public", + x: 0, + }); + + const page = await openPage({ + room: privateRoom, + run, + user: 1, + visibility: "private", + permissions: PUBLIC_COMMENTS_WRITE_PRIVATE_NONE_PERMISSIONS, + mode: "create", + x: 0, + }); + + await waitForCreatePageLoaded(page); + await page.click("#create-thread"); + await waitForJson(page, "#errorContextType", "CREATE_THREAD_ERROR", SLOW); + await expect(page.locator("#errorCause")).toContainText( + /forbidden|permission|unauthorized|not allowed|403/i, + SLOW + ); + await page.close(); + + const verifierPage = await openPage({ + room: privateRoom, + run, + user: 1, + visibility: "all", + x: 0, + }); + + await waitForPageLoaded(verifierPage); + await waitForJson(verifierPage, "#threadCount", 0, SLOW); + await verifierPage.close(); + } + ); + + async function openPage({ + room, + run, + user, + visibility, + permissions, + mode, + x, + }: { + room: string; + run: string; + user: number; + visibility: "all" | "public" | "private"; + permissions?: readonly string[]; + mode?: "read" | "create"; + x: number; + }) { + const page = await preparePage( + getPageUrl({ + room, + run, + user, + visibility, + permissions, + mode, + }), + { x } + ); + pages.push(page); + return page; + } + + async function createThreadWithVisibilityPermissions({ + room, + run, + user, + visibility, + x, + }: { + room: string; + run: string; + user: number; + visibility: "public" | "private"; + x: number; + }) { + const page = await openPage({ + room, + run, + user, + visibility, + permissions: VISIBILITY_PERMISSIONS[visibility], + mode: "create", + x, + }); + + await waitForCreatePageLoaded(page); + + const createThreadResponse$ = waitForCreateThreadResponse(page); + await page.click("#create-thread"); + await createThreadResponse$; + await page.close(); + } + + async function createThreadWithExplicitPermissions({ + room, + run, + user, + visibility, + permissions, + x, + }: { + room: string; + run: string; + user: number; + visibility: "public" | "private"; + permissions: readonly string[]; + x: number; + }) { + const page = await openPage({ + room, + run, + user, + visibility, + permissions, + mode: "create", + x, + }); + + await waitForCreatePageLoaded(page); + + const createThreadResponse$ = waitForCreateThreadResponse(page); + await page.click("#create-thread"); + await createThreadResponse$; + await page.close(); + } + + async function verifyPersistedVisibility({ + room, + run, + user, + visibility, + x, + }: { + room: string; + run: string; + user: number; + visibility: "public" | "private"; + x: number; + }) { + const page = await openPage({ + room, + run, + user, + visibility: "all", + x, + }); + + await waitForPageLoaded(page); + await waitForJson(page, "#threadCount", 1, SLOW); + await waitForJson( + page, + visibility === "public" ? "#publicThreadCount" : "#privateThreadCount", + 1, + SLOW + ); + await waitForJson(page, "#threadVisibilities", [visibility], SLOW); + await page.close(); + } +}); + +function getRoomId(testInfo: TestInfo, run: string, suffix: string) { + return genRoomId(testInfo, `:${run}:${suffix}`); +} + +function getPageUrl({ + room, + run, + user, + visibility, + permissions, + mode, +}: { + room: string; + run: string; + user: number; + visibility: "all" | "public" | "private"; + permissions?: readonly string[]; + mode?: "read" | "create"; +}) { + const url = new URL(TEST_URL); + url.searchParams.set("room", room); + url.searchParams.set("run", run); + url.searchParams.set("user", String(user)); + url.searchParams.set("visibility", visibility); + for (const permission of permissions ?? []) { + url.searchParams.append("permissions", permission); + } + if (mode !== undefined) { + url.searchParams.set("mode", mode); + } + return url.toString(); +} + +async function waitForPageLoaded(page: Page) { + await waitForJson(page, "#isLoading", false, SLOW); + await waitForJson(page, "#error", undefined, SLOW); +} + +async function waitForCreatePageLoaded(page: Page) { + await waitForJson(page, "#error", undefined, SLOW); + await expect(page.locator("#create-thread")).toBeVisible(SLOW); +} + +async function waitForCreateThreadResponse(page: Page) { + const response = await page.waitForResponse((candidate) => { + if (candidate.request().method() !== "POST") { + return false; + } + + return new URL(candidate.url()).pathname.endsWith("/threads"); + }, SLOW); + + expect(response.ok()).toBe(true); +} diff --git a/e2e/next-sandbox/test/utils.ts b/e2e/next-sandbox/test/utils.ts index 071eb55c8ad..7580b138c4c 100644 --- a/e2e/next-sandbox/test/utils.ts +++ b/e2e/next-sandbox/test/utils.ts @@ -10,6 +10,8 @@ export type IDSelector = `#${string}`; const WIDTH = 640; const HEIGHT = 800; +const DEFAULT_ROOM_ID_MAX_LENGTH = 100; +const ABSOLUTE_ROOM_ID_MAX_LENGTH = 128; function getTestFilename(fullPath: string): string { const parts = fullPath.split("/"); @@ -25,7 +27,7 @@ function getTestFilename(fullPath: string): string { * filename and the full test name. Additionally, will prepend the Git SHA if * available (e.g. when running in CI). */ -export function genRoomId(testInfo: TestInfo) { +export function genRoomId(testInfo: TestInfo, suffix = "") { const prefix = process.env.NEXT_PUBLIC_GITHUB_SHA ? process.env.NEXT_PUBLIC_GITHUB_SHA.slice(0, 2) : null; @@ -38,16 +40,25 @@ export function genRoomId(testInfo: TestInfo) { .replace(/^-+/, "") .replace(/-+$/, ""); let roomId = `e2e:${title}`; - if (roomId.length > 100) { + const maxLength = + suffix.length === 0 + ? DEFAULT_ROOM_ID_MAX_LENGTH + : ABSOLUTE_ROOM_ID_MAX_LENGTH; + const maxBaseLength = maxLength - suffix.length; + if (maxBaseLength < 8) { + throw new Error(`Room ID suffix is too long: ${suffix.length}`); + } + + if (roomId.length > maxBaseLength) { // Room IDs cannot be longer than 128 chars. If this happens, take a short // hash from the full room ID, then cut it off and attach the hash. This // way, test names can still be arbitrarily long, human-readable (at least // the first part of it), and yet still stable for reuse, so we don't have // an ever-growing set of rooms when running against DEV or PROD. const hash = hash7(roomId); - roomId = roomId.slice(0, 100 - hash.length) + hash; + roomId = roomId.slice(0, maxBaseLength - hash.length) + hash; } - return roomId; + return `${roomId}${suffix}`; } /** diff --git a/guides/guides.json b/guides/guides.json index c7580046415..fa37bb26011 100644 --- a/guides/guides.json +++ b/guides/guides.json @@ -511,5 +511,19 @@ "topics": ["presence", "tutorials", "data-fetching"], "technologies": ["react-ui", "react"], "date": "2026-03-04" + }, + { + "title": "How to use public and private threads", + "path": "/how-to-use-public-and-private-threads", + "topics": ["comments", "authentication"], + "technologies": ["react", "nodejs"], + "date": "2026-06-23" + }, + { + "title": "How to add private commenting to your app", + "path": "/how-to-add-private-commenting-to-your-app", + "topics": ["comments", "authentication"], + "technologies": ["react", "nextjs"], + "date": "2026-06-24" } ] diff --git a/guides/pages/how-to-add-private-commenting-to-your-app.mdx b/guides/pages/how-to-add-private-commenting-to-your-app.mdx new file mode 100644 index 00000000000..d97e95b3783 --- /dev/null +++ b/guides/pages/how-to-add-private-commenting-to-your-app.mdx @@ -0,0 +1,195 @@ +--- +meta: + title: "How to add private commenting to your app" + description: + "Learn how to add private comments for admins and team members, alongside + your normal public comments." +--- + +Some apps have comments that only certain people should see—internal notes, +moderation discussions, or team-only annotations. With Liveblocks Comments you +can add these alongside your normal public comments, for example with a separate +“Leave note” button next to the regular comment button. + + + +Private threads are only available on Team and Enterprise plans. + + + +## What we’re building + +This guide shows how to give two different thread permissions to two different +groups of users, **admins** and **regular users**. Admins will be able to view +both public and private threads, whereas regular users will only be able to view +public threads. + + + +This guide focuses on building a private commenting UI. For the complete +visibility and permissions reference, see +[How to use public and private threads](/docs/guides/how-to-use-public-and-private-threads) +and [Permissions](/docs/authentication/permissions). + + + +## How private threads work + +A thread’s _visibility_ is set when it’s created, and every comment inside it +shares the same visibility. Thread visibility is public by default, but a +private thread can be created by passing the `visibility="private"` option. +Liveblocks then only delivers private threads to users who have permission to +read them. + +There are three pieces to building this: + +1. [Authenticate users](/docs/authentication), and give admins and regular users + different room permissions. +2. Create private threads with + [`Composer`](/docs/api-reference/liveblocks-react-ui#Creating-private-threads) + or [`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread). +3. Render threads differently depending on their `visibility` property. + +## Authenticate users + +Before we get started, we need to decide on +[permissions](/docs/authentication/permissions) for each group. Admin users will +have full access to all threads, whereas regular users will only be able to see +public threads. + + + +| Group | Permissions | Description | +| ----------- | -------------------------------------- | -------------------------------------- | +| `"admin"` | `["*:write"]` | Full access, including all threads. | +| `"regular"` | `["*:write", "comments:private:none"]` | Full access, but hide private threads. | + +
+ +When creating a room, you can set these permissions for each group under +`groupAccesses`. + +```ts +const room = await liveblocks.createRoom("my-room-id", { + groupAccesses: { + // +++ + admin: ["*:write"], + regular: ["*:write", "comments:private:none"], + //+++ + }, +}); +``` + +To assign users to these groups, use `groupIds` in +[identifyUser](/docs/api-reference/liveblocks-node#id-tokens), inside your +authentication endpoint. + +```ts +// `marc` is an admin user +const { body, status } = await liveblocks.identifyUser({ + userId: "marc", + // +++ + groupIds: ["admin"], + // +++ +}); + +// `olivier` is a regular user +const { body, status } = await liveblocks.identifyUser({ + userId: "olivier", + // +++ + groupIds: ["regular"], + // +++ +}); +``` + +Permissions and authentication are now set up! + + + +Liveblocks recommends [ID token authentication](/docs/authentication) by +default, which we’ve used in these code snippets. If you’re using +[access tokens](/docs/authentication/access-token), you must set permissions in +[`prepareSession`](/docs/api-reference/liveblocks-node#access-tokens) instead of +on the room. + + + +## Create a private thread + +To create a private thread, pass the `visibility="private"` option to the +[`Composer`](/docs/api-reference/liveblocks-react-ui#Composer). In your app, you +may wish to hide this behind a button that only admins can see. + +```tsx +import { Composer } from "@liveblocks/react-ui"; + +function PrivateCommentComposer() { + return ( + // +++ + + // +++ + ); +} +``` + +For more complex use cases, you can use the +[`useCreateThread`](/docs/api-reference/liveblocks-react#useCreateThread) hook +to create a private thread. + +## Render threads in your app + +To render threads in your app, use +[`useThreads`](/docs/api-reference/liveblocks-react#useThreads) as usual, and +each will only see the threads they are allowed to read. Check for the +`visibility` property to render different UI for public and private threads. + +```tsx title="Public threads only" +import { Thread } from "@liveblocks/react-ui"; +import { useThreads } from "@liveblocks/react/suspense"; + +function ThreadList() { + // +++ + const { threads } = useThreads(); + // +++ + + return ( + <> + {threads.map((thread) => ( +
+ // +++ + {thread.visibility === "private" && ( +
Private, only your team can see this
+ )} + // +++ + +
+ ))} + + ); +} +``` + +### Filtering private threads + +If you’d like for admin users to _only_ see public or private threads, you can +use the `query` option to filter threads by visibility. + +```tsx title="Public threads only" +const { threads } = useThreads({ + query: { + // +++ + visibility: "private", + // +++ + }, +}); +``` + +## Next steps + +You now have public and private comments living side by side in the same room. +Here’s where to learn more: + +- [How to use public and private threads](/docs/guides/how-to-use-public-and-private-threads) +- [Permissions](/docs/authentication/permissions) +- [Comments API reference](/docs/api-reference/liveblocks-react#Comments) +- [Comments component reference](/docs/api-reference/liveblocks-react-ui#Comments) diff --git a/guides/pages/how-to-filter-threads-using-query-language.mdx b/guides/pages/how-to-filter-threads-using-query-language.mdx index c33cdd0fd2a..b7554eaa238 100644 --- a/guides/pages/how-to-filter-threads-using-query-language.mdx +++ b/guides/pages/how-to-filter-threads-using-query-language.mdx @@ -17,9 +17,9 @@ to have filtering that works the same as with ## Query language -You can filter threads by their metadata, allowing you to select for certain -properties, values, or even for string prefixes. Filters can be combined using -`AND` logic. +You can filter threads by their metadata, resolved status, or visibility, +allowing you to select for certain properties, values, or even for string +prefixes. Filters can be combined using `AND` logic. ```js // Resolved threads @@ -40,6 +40,9 @@ metadata['pinned']:false // Threads without a `color` property metadata['color']:null +// Private threads +visibility:'private' + // Combine queries with AND resolved:true AND metadata['priority']:3 diff --git a/guides/pages/how-to-use-public-and-private-threads.mdx b/guides/pages/how-to-use-public-and-private-threads.mdx new file mode 100644 index 00000000000..f9ea5afd576 --- /dev/null +++ b/guides/pages/how-to-use-public-and-private-threads.mdx @@ -0,0 +1,304 @@ +--- +meta: + title: "How to use public and private threads" + description: + "Learn how to create public and private threads, configure permissions, and + show filtered comment views." +--- + +Public and private threads let you separate the threads users can see in the +same Liveblocks room. + +Visibility is set on a thread and all comments inside that thread use the same +visibility. Set visibility when the thread is created; existing threads keep +their current visibility. + + + +Private threads are only available on Team and Enterprise plans. + + + +## How it works + +Comment threads are public by default. To create a private thread, set +`visibility: "private"` when the thread is created. + +```tsx +import { Composer } from "@liveblocks/react-ui"; + +function PrivateComposer() { + return ; +} +``` + +Liveblocks checks permissions when threads are created and retrieved: + +- Users need `comments:public:read` or broader read access to receive public + threads. +- Users need `comments:private:read` or broader read access to receive private + threads. +- Users need `comments:private:write` or broader write access to create private + threads. + +The broader `comments:read`, `comments:write`, and `comments:none` permissions +apply to both public and private threads. Use `comments:public:*` and +`comments:private:*` to override one visibility. + +## Set up permissions + +Start by deciding which users can read or write private threads. For example, +you might allow all users to write public threads, but only reviewers to read +and write private threads. + +Every room permission list needs a base permission such as `*:read` or +`*:write`. Then add granular comments permissions to override access for public +or private threads. + +```ts +const regularUserPermissions = [ + "*:read", + "comments:public:write", + "comments:private:none", +]; + +const reviewerPermissions = [ + "*:read", + "comments:public:write", + "comments:private:write", +]; +``` + +### With ID tokens + +With [ID token authentication](/docs/authentication#id-token-room-permissions), +set permissions on the room using `defaultAccesses`, `groupsAccesses`, or +`usersAccesses`. + +```ts +import { Liveblocks } from "@liveblocks/node"; + +const liveblocks = new Liveblocks({ + secret: "{{SECRET_KEY}}", +}); + +await liveblocks.createRoom("my-room-id", { + // Everyone can read the room and create public threads + defaultAccesses: ["*:read", "comments:public:write", "comments:private:none"], + + // Reviewers can also read and create private threads + groupsAccesses: { + reviewers: ["*:read", "comments:public:write", "comments:private:write"], + }, +}); +``` + +If the room already exists, use +[`liveblocks.updateRoom`](/docs/api-reference/liveblocks-node#post-rooms-roomId) +with the same permission fields. + +### With access tokens + +With +[access token authentication](/docs/authentication/access-token#room-permissions), +grant the same permission lists when you prepare the user's session. + +```ts +import { Liveblocks } from "@liveblocks/node"; + +const liveblocks = new Liveblocks({ + secret: "{{SECRET_KEY}}", +}); + +const session = liveblocks.prepareSession("marie@example.com"); + +session.allow("my-room-id", [ + "*:read", + "comments:public:write", + "comments:private:none", +]); + +const { body, status } = await session.authorize(); +``` + +Grant private comments access only for users that should see private threads. + +```ts +session.allow("my-room-id", [ + "*:read", + "comments:public:write", + "comments:private:write", +]); +``` + +## Create private threads + +The default [`Composer`][] creates public threads unless you pass +`visibility="private"`. + +```tsx +import { Composer } from "@liveblocks/react-ui"; + +function Comments() { + return ( + <> + + + + ); +} +``` + +The private composer is disabled when the current user does not have write +access to private threads. + +### With mutation hooks + +If you're building a custom composer, pass `visibility: "private"` to +[`useCreateThread`][]. + +```tsx +import { type CommentBody } from "@liveblocks/client"; +import { useCreateThread } from "@liveblocks/react/suspense"; + +function CreatePrivateThreadButton({ body }: { body: CommentBody }) { + const createThread = useCreateThread(); + + return ( + + ); +} +``` + +### From your server + +To create private threads from your back end, pass `visibility: "private"` to +[`liveblocks.createThread`][]. + +```ts +await liveblocks.createThread({ + roomId: "my-room-id", + data: { + visibility: "private", + comment: { + userId: "marie@example.com", + body: { + version: 1, + content: [{ type: "paragraph", children: [{ text: "Internal note" }] }], + }, + }, + }, +}); +``` + +## Show threads in your app + +Use [`useThreads`][] as usual. Users only receive threads they have permission +to read. + +```tsx +import { Thread } from "@liveblocks/react-ui"; +import { useThreads } from "@liveblocks/react/suspense"; + +function ThreadList() { + const { threads } = useThreads(); + + return ( + <> + {threads.map((thread) => ( + + ))} + + ); +} +``` + +A user without private comments access won't receive private threads from +client-side APIs such as [`useThreads`][] or [`room.getThreads`][]. + +## Filter public and private views + +Filtering is optional. Use it when you want separate UI views for public and +private discussions. + +```tsx +const { threads: publicThreads } = useThreads({ + query: { + visibility: "public", + }, +}); + +const { threads: privateThreads } = useThreads({ + query: { + visibility: "private", + }, +}); +``` + +You can combine visibility with the other thread query options. + +```tsx +const { threads } = useThreads({ + query: { + visibility: "private", + resolved: false, + metadata: { + status: "open", + }, + }, +}); +``` + +On the server, use the same query object with [`liveblocks.getThreads`][]. + +```ts +const { data: threads } = await liveblocks.getThreads({ + roomId: "my-room-id", + query: { + visibility: "private", + metadata: { + status: "open", + }, + }, +}); +``` + +The REST API also supports visibility in the +[thread query language](/docs/guides/how-to-filter-threads-using-query-language). + +```js +visibility:'private' AND metadata['status']:'open' +``` + +## Troubleshooting + +If a user can't see private threads, check that their room permission list +includes `comments:private:read`, `comments:private:write`, or a broader +permission such as `comments:read` or `*:read`, with no more specific +`comments:private:none` override. + +If a user can't create private threads, check that their room permission list +includes `comments:private:write`, `comments:write`, or `*:write`, with no more +specific `comments:private:none` or `comments:private:read` override. + +If `useThreads({ query: { visibility: "private" } })` returns an empty list for +one user but not another, the first user probably does not have read access to +private threads. + +[`Composer`]: /docs/api-reference/liveblocks-react-ui#Composer +[`liveblocks.createThread`]: + /docs/api-reference/liveblocks-node#post-rooms-roomId-threads +[`liveblocks.getThreads`]: + /docs/api-reference/liveblocks-node#get-rooms-roomId-threads +[`room.getThreads`]: /docs/api-reference/liveblocks-client#Room.getThreads +[`useCreateThread`]: /docs/api-reference/liveblocks-react#useCreateThread +[`useThreads`]: /docs/api-reference/liveblocks-react#useThreads diff --git a/packages/liveblocks-chat-sdk-adapter/src/__tests__/index.test.ts b/packages/liveblocks-chat-sdk-adapter/src/__tests__/index.test.ts index 1bd79cf2a3f..57e62a42298 100644 --- a/packages/liveblocks-chat-sdk-adapter/src/__tests__/index.test.ts +++ b/packages/liveblocks-chat-sdk-adapter/src/__tests__/index.test.ts @@ -5,6 +5,7 @@ import type { CommentData, ResolveGroupsInfoArgs, ResolveUsersArgs, + ThreadVisibility, } from "@liveblocks/core"; import type { ChatInstance, Root } from "chat"; import { beforeEach, describe, expect, test, vi } from "vitest"; @@ -109,6 +110,7 @@ function createDummyThread( createdAt: Date; updatedAt: Date; resolved: boolean; + visibility: ThreadVisibility; metadata: Record; }> ) { @@ -121,6 +123,7 @@ function createDummyThread( comments, metadata: {}, resolved: false, + visibility: "public", ...overrides, }; } @@ -2084,6 +2087,7 @@ describe("LiveblocksAdapter", () => { createdAt: new Date(), updatedAt: new Date(), resolved: false, + visibility: "public", }); const result = await adapter.postChannelMessage( @@ -2116,6 +2120,7 @@ describe("LiveblocksAdapter", () => { createdAt: new Date(), updatedAt: new Date(), resolved: false, + visibility: "public", }); await expect( diff --git a/packages/liveblocks-core/src/__tests__/auth-manager.test.ts b/packages/liveblocks-core/src/__tests__/auth-manager.test.ts index c19b55f3777..4622b7d54aa 100644 --- a/packages/liveblocks-core/src/__tests__/auth-manager.test.ts +++ b/packages/liveblocks-core/src/__tests__/auth-manager.test.ts @@ -11,9 +11,16 @@ import { vi, } from "vitest"; -import { createAuthManager } from "../auth-manager"; +import { createApiClient } from "../api-client"; +import { type AuthRequest, createAuthManager } from "../auth-manager"; +import { DEFAULT_BASE_URL } from "../constants"; import { Permission, type RoomPermissions } from "../permissions"; import type { ParsedAuthToken } from "../protocol/AuthToken"; +import type { + BaseMetadata, + CommentBody, + ThreadDataPlain, +} from "../protocol/Comments"; const SECONDS = 1 * 1000; const MINUTES = 60 * SECONDS; @@ -284,6 +291,280 @@ describe("auth-manager - secret auth", () => { expect(requestCount).toBe(1); }); + test("should reuse broad comments token for scoped comments read requests", async () => { + let localRequestCount = 0; + const commentsReadToken = makeAccessToken({ + "org1*": [Permission.Read, Permission.CommentsRead], + }); + + server.use( + http.post("/api/access-auth-comments-read", () => { + localRequestCount++; + return HttpResponse.json({ token: commentsReadToken }); + }) + ); + + const authManager = createAuthManager({ + authEndpoint: "/api/access-auth-comments-read", + }); + + const commentsReadAuthValue = (await authManager.getAuthValue({ + resource: "comments", + access: "read", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + const publicReadAuthValue = (await authManager.getAuthValue({ + resource: "comments:public", + access: "read", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + + expect(commentsReadAuthValue.token.raw).toEqual(commentsReadToken); + expect(publicReadAuthValue.token.raw).toEqual(commentsReadToken); + expect(localRequestCount).toBe(1); + }); + + test("should reuse scoped comments read token for generic comments read requests", async () => { + let localRequestCount = 0; + const publicCommentsReadToken = makeAccessToken({ + "org1*": [ + Permission.Read, + Permission.CommentsNone, + Permission.CommentsPublicRead, + ], + }); + + server.use( + http.post("/api/access-auth-public-comments-read", () => { + localRequestCount++; + return HttpResponse.json({ token: publicCommentsReadToken }); + }) + ); + + const authManager = createAuthManager({ + authEndpoint: "/api/access-auth-public-comments-read", + }); + + const publicReadAuthValue = (await authManager.getAuthValue({ + resource: "comments:public", + access: "read", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + const commentsReadAuthValue = (await authManager.getAuthValue({ + resource: "comments", + access: "read", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + + expect(publicReadAuthValue.token.raw).toEqual(publicCommentsReadToken); + expect(commentsReadAuthValue.token.raw).toEqual(publicCommentsReadToken); + expect(localRequestCount).toBe(1); + }); + + test("should reuse scoped comments write token for generic comments write requests", async () => { + let localRequestCount = 0; + const publicCommentsWriteToken = makeAccessToken({ + "org1*": [ + Permission.Read, + Permission.CommentsNone, + Permission.CommentsPublicWrite, + ], + }); + + server.use( + http.post("/api/access-auth-public-then-comments-write", () => { + localRequestCount++; + return HttpResponse.json({ token: publicCommentsWriteToken }); + }) + ); + + const authManager = createAuthManager({ + authEndpoint: "/api/access-auth-public-then-comments-write", + }); + + const publicWriteAuthValue = (await authManager.getAuthValue({ + resource: "comments:public", + access: "write", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + const commentsWriteAuthValue = (await authManager.getAuthValue({ + resource: "comments", + access: "write", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + + expect(publicWriteAuthValue.token.raw).toEqual(publicCommentsWriteToken); + expect(commentsWriteAuthValue.token.raw).toEqual(publicCommentsWriteToken); + expect(localRequestCount).toBe(1); + }); + + test("should not reuse public comments token for private comments requests", async () => { + let localRequestCount = 0; + const publicCommentsReadToken = makeAccessToken({ + "org1*": [ + Permission.Read, + Permission.CommentsNone, + Permission.CommentsPublicRead, + ], + }); + const privateCommentsReadToken = makeAccessToken({ + "org1*": [ + Permission.Read, + Permission.CommentsNone, + Permission.CommentsPrivateRead, + ], + }); + + server.use( + http.post("/api/access-auth-public-then-private-comments-read", () => { + localRequestCount++; + return HttpResponse.json({ + token: + localRequestCount === 1 + ? publicCommentsReadToken + : privateCommentsReadToken, + }); + }) + ); + + const authManager = createAuthManager({ + authEndpoint: "/api/access-auth-public-then-private-comments-read", + }); + + const publicReadAuthValue = (await authManager.getAuthValue({ + resource: "comments:public", + access: "read", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + const privateReadAuthValue = (await authManager.getAuthValue({ + resource: "comments:private", + access: "read", + roomId: "org1.room1", + })) as { type: "secret"; token: ParsedAuthToken }; + + expect(publicReadAuthValue.token.raw).toEqual(publicCommentsReadToken); + expect(privateReadAuthValue.token.raw).toEqual(privateCommentsReadToken); + expect(localRequestCount).toBe(2); + }); + + test("api client should request concrete auth for known visibility and generic auth for unknown visibility", async () => { + const commentBody = { + version: 1, + content: [], + } satisfies CommentBody; + const thread = { + type: "thread", + id: "th_123", + roomId: "room-id", + createdAt: new Date(0).toISOString(), + updatedAt: new Date(0).toISOString(), + comments: [], + metadata: {}, + resolved: false, + visibility: "public", + } satisfies ThreadDataPlain; + + server.use( + http.get(`${DEFAULT_BASE_URL}/v2/c/rooms/:roomId/threads`, () => { + return HttpResponse.json({ + data: [], + inboxNotifications: [], + subscriptions: [], + meta: { + requestedAt: new Date(0).toISOString(), + nextCursor: null, + permissionHints: {}, + }, + }); + }), + http.post(`${DEFAULT_BASE_URL}/v2/c/rooms/:roomId/threads`, () => + HttpResponse.json(thread) + ), + http.post( + `${DEFAULT_BASE_URL}/v2/c/rooms/:roomId/threads/:threadId/mark-as-resolved`, + () => HttpResponse.json({}) + ) + ); + + async function expectAuthRequest( + run: ( + client: ReturnType> + ) => Promise, + expected: AuthRequest + ) { + const authRequests: AuthRequest[] = []; + const client = createApiClient({ + baseUrl: DEFAULT_BASE_URL, + fetchPolyfill: globalThis.fetch?.bind(globalThis), + authManager: { + reset() {}, + getAuthValue(request) { + authRequests.push(request); + return Promise.resolve({ + type: "public", + publicApiKey: "pk_test", + }); + }, + }, + }); + + await run(client); + + expect(authRequests).toEqual([expected]); + } + + await expectAuthRequest( + (client) => + client.getThreads({ + roomId: "room-id", + query: { visibility: "private" }, + }), + { roomId: "room-id", resource: "comments:private", access: "read" } + ); + + await expectAuthRequest( + (client) => + client.createThread({ + roomId: "room-id", + metadata: {}, + commentMetadata: undefined, + body: commentBody, + }), + { roomId: "room-id", resource: "comments:public", access: "write" } + ); + + await expectAuthRequest( + (client) => + client.createThread({ + roomId: "room-id", + visibility: "private", + metadata: {}, + commentMetadata: undefined, + body: commentBody, + }), + { roomId: "room-id", resource: "comments:private", access: "write" } + ); + + await expectAuthRequest( + (client) => + client.markThreadAsResolved({ + roomId: "room-id", + threadId: "th_123", + visibility: "private", + }), + { roomId: "room-id", resource: "comments:private", access: "write" } + ); + + await expectAuthRequest( + (client) => + client.markThreadAsResolved({ + roomId: "room-id", + threadId: "th_123", + }), + { roomId: "room-id", resource: "comments", access: "write" } + ); + }); + test("should fetch a new token when cached comments read token cannot write", async () => { let localRequestCount = 0; const commentsReadToken = makeAccessToken({ diff --git a/packages/liveblocks-core/src/__tests__/permissions.test.ts b/packages/liveblocks-core/src/__tests__/permissions.test.ts index 0a6335b1610..ab7254ee10e 100644 --- a/packages/liveblocks-core/src/__tests__/permissions.test.ts +++ b/packages/liveblocks-core/src/__tests__/permissions.test.ts @@ -20,6 +20,8 @@ const PERMISSION_RESOURCES = [ "room", "storage", "comments", + "comments:public", + "comments:private", "feeds", "personal", ] as const satisfies readonly PermissionResources[]; @@ -57,13 +59,6 @@ function accessRank(resource: PermissionResources, matrix: PermissionMatrix) { return ACCESS_LEVEL_RANKS[matrix[resource]]; } -function hasExplicitFeatureScope( - scopes: readonly Permission[], - resource: "storage" | "comments" | "feeds" -): boolean { - return scopes.some((scope) => scope.startsWith(`${resource}:`)); -} - function mergeRoomPermissionMatrix({ defaultAccesses, groupsAccesses, @@ -114,7 +109,7 @@ describe("normalizeRoomPermissions", () => { describe("permissionMatrixFromScopes", () => { test("resolves read access", () => { - expect(permissionMatrixFromScopes([Permission.Read])).toEqual({ + expect(permissionMatrixFromScopes([Permission.Read])).toMatchObject({ room: "read", storage: "read", comments: "read", @@ -124,7 +119,7 @@ describe("permissionMatrixFromScopes", () => { }); test("resolves write access", () => { - expect(permissionMatrixFromScopes([Permission.Write])).toEqual({ + expect(permissionMatrixFromScopes([Permission.Write])).toMatchObject({ room: "write", storage: "write", comments: "write", @@ -133,6 +128,21 @@ describe("permissionMatrixFromScopes", () => { }); }); + test("inherits base and broad comments access to scoped comments permissions", () => { + expect(permissionMatrixFromScopes([Permission.Write])).toMatchObject({ + comments: "write", + "comments:public": "write", + "comments:private": "write", + }); + expect( + permissionMatrixFromScopes([Permission.Read, Permission.CommentsWrite]) + ).toMatchObject({ + comments: "write", + "comments:public": "write", + "comments:private": "write", + }); + }); + test("resolves room read and write as aliases", () => { expect(permissionMatrixFromScopes([Permission.RoomRead])).toEqual( permissionMatrixFromScopes([Permission.Read]) @@ -183,6 +193,50 @@ describe("permissionMatrixFromScopes", () => { expect(hasPermissionAccess(matrix, "comments", "write")).toBe(false); }); + test("resolves broad and visibility-specific comments permissions", () => { + const broad = permissionMatrixFromScopes([ + Permission.Write, + Permission.CommentsRead, + ]); + + expect(broad).toMatchObject({ + comments: "read", + "comments:public": "read", + "comments:private": "read", + }); + + const scoped = permissionMatrixFromScopes([ + Permission.Write, + Permission.CommentsNone, + Permission.CommentsPublicRead, + Permission.CommentsPrivateWrite, + ]); + + expect(scoped).toMatchObject({ + comments: "none", + "comments:public": "read", + "comments:private": "write", + }); + expect(hasPermissionAccess(scoped, "comments", "write")).toBe(true); + expect(hasPermissionAccess(scoped, "comments:public", "write")).toBe(false); + expect(hasPermissionAccess(scoped, "comments:private", "write")).toBe(true); + }); + + test("treats generic comments checks as aggregate comments access", () => { + const matrix = permissionMatrixFromScopes([ + Permission.Write, + Permission.CommentsNone, + Permission.CommentsPublicWrite, + Permission.CommentsPrivateNone, + ]); + + expect(hasPermissionAccess(matrix, "comments", "write")).toBe(true); + expect(hasPermissionAccess(matrix, "comments:public", "write")).toBe(true); + expect(hasPermissionAccess(matrix, "comments:private", "write")).toBe( + false + ); + }); + test("feature permissions require a base permission", () => { expect( hasPermissionAccess( @@ -237,7 +291,7 @@ describe("permissionMatrixFromScopes", () => { [{ pattern: "org1*", scopes: [Permission.RoomWrite] }], "org1.room1" ) - ).toEqual({ + ).toMatchObject({ room: "write", storage: "write", comments: "write", @@ -312,15 +366,29 @@ describe("permission matrix helpers", () => { test("serializes permission matrix to minimal scopes", () => { expect( - permissionMatrixToScopes({ - room: "read", - storage: "none", - comments: "read", - feeds: "read", - personal: "write", - }) + permissionMatrixToScopes( + permissionMatrixFromScopes([Permission.Read, Permission.StorageNone]) + ) ).toEqual([Permission.Read, Permission.StorageNone]); }); + + test("serializes split comment scopes when public and private differ", () => { + expect( + permissionMatrixToScopes( + permissionMatrixFromScopes([ + Permission.Read, + Permission.CommentsWrite, + Permission.CommentsPublicRead, + Permission.CommentsPrivateNone, + ]) + ) + ).toEqual([ + Permission.Read, + Permission.CommentsWrite, + Permission.CommentsPublicRead, + Permission.CommentsPrivateNone, + ]); + }); }); describe("mergeRoomPermissionScopes", () => { @@ -346,7 +414,7 @@ describe("mergeRoomPermissionScopes", () => { userAccesses: [Permission.Read, Permission.StorageWrite], }) ) - ).toEqual({ + ).toMatchObject({ room: "read", storage: "write", comments: "read", @@ -388,7 +456,7 @@ describe("mergeRoomPermissionScopes", () => { userAccesses: [], }) ) - ).toEqual({ + ).toMatchObject({ room: "read", storage: "read", comments: "read", @@ -476,6 +544,16 @@ describe("validatePermissionsSet", () => { ).toBe(true); }); + test("accepts scoped comments permissions together", () => { + expect( + validatePermissionsSet([ + Permission.Read, + Permission.CommentsPublicWrite, + Permission.CommentsPrivateNone, + ]) + ).toBe(true); + }); + test("accepts the legacy presence scope as an extra room scope", () => { expect( validatePermissionsSet([ @@ -517,6 +595,18 @@ describe("validatePermissionsSet", () => { 'Permissions can include at most one scope per feature, got multiple "comments" scopes' ); }); + + test("rejects multiple scopes for the same scoped comments feature", () => { + expect( + validatePermissionsSet([ + Permission.Read, + Permission.CommentsPublicRead, + Permission.CommentsPublicWrite, + ]) + ).toBe( + 'Permissions can include at most one scope per feature, got multiple "comments:public" scopes' + ); + }); }); describe("property tests", () => { @@ -611,6 +701,22 @@ describe("property tests", () => { const matrix = permissionMatrixFromScopes(scopes); for (const resource of PERMISSION_RESOURCES) { + if (resource === "comments") { + const commentsAccessRank = Math.max( + accessRank("comments", matrix), + accessRank("comments:public", matrix), + accessRank("comments:private", matrix) + ); + + expect(hasPermissionAccess(matrix, resource, "read")).toBe( + commentsAccessRank >= ACCESS_LEVEL_RANKS.read + ); + expect(hasPermissionAccess(matrix, resource, "write")).toBe( + commentsAccessRank >= ACCESS_LEVEL_RANKS.write + ); + continue; + } + expect(hasPermissionAccess(matrix, resource, "read")).toBe( accessRank(resource, matrix) >= ACCESS_LEVEL_RANKS.read ); @@ -703,49 +809,6 @@ describe("property tests", () => { ); }); - test("mergeRoomPermissionScopes takes the highest explicit access per feature across groups", () => { - fc.assert( - fc.property(validScopeSet, validScopeSet, (left, right) => { - const merged = mergeRoomPermissionMatrix({ - defaultAccesses: [], - groupsAccesses: [left, right], - userAccesses: [], - }); - const leftMatrix = permissionMatrixFromScopes(left); - const rightMatrix = permissionMatrixFromScopes(right); - - if (!hasBasePermission(left) && !hasBasePermission(right)) { - expect(merged).toEqual(permissionMatrixFromScopes([])); - return; - } - - const expectedRoom = Math.max( - hasBasePermission(left) ? accessRank("room", leftMatrix) : 0, - hasBasePermission(right) ? accessRank("room", rightMatrix) : 0 - ); - expect(accessRank("room", merged)).toBe(expectedRoom); - - for (const resource of ["storage", "comments", "feeds"] as const) { - const explicitRanks = [left, right] - .filter((scopes) => hasExplicitFeatureScope(scopes, resource)) - .map((scopes) => - accessRank(resource, permissionMatrixFromScopes(scopes)) - ); - - if (explicitRanks.length === 0) { - expect(accessRank(resource, merged)).toBe( - accessRank("room", merged) - ); - } else { - expect(accessRank(resource, merged)).toBe( - Math.max(...explicitRanks) - ); - } - } - }) - ); - }); - test("mergeRoomPermissionScopes lets user base permissions replace lower layers", () => { fc.assert( fc.property(mergeRoomPermissionInputs, (inputs) => { @@ -760,23 +823,4 @@ describe("property tests", () => { }) ); }); - - test("mergeRoomPermissionScopes lets explicit user feature scopes override lower layers", () => { - fc.assert( - fc.property(mergeRoomPermissionInputs, (inputs) => { - if (!hasBasePermission(inputs.userAccesses)) { - return; - } - - const matrix = mergeRoomPermissionMatrix(inputs); - const userMatrix = permissionMatrixFromScopes(inputs.userAccesses); - - for (const resource of ["storage", "comments", "feeds"] as const) { - if (hasExplicitFeatureScope(inputs.userAccesses, resource)) { - expect(matrix[resource]).toBe(userMatrix[resource]); - } - } - }) - ); - }); }); diff --git a/packages/liveblocks-core/src/api-client.ts b/packages/liveblocks-core/src/api-client.ts index f366abf7e34..798fabc64d8 100644 --- a/packages/liveblocks-core/src/api-client.ts +++ b/packages/liveblocks-core/src/api-client.ts @@ -25,7 +25,7 @@ import { stringifyOrLog as stringify } from "./lib/stringify"; import type { QueryParams, URLSafeString } from "./lib/url"; import { url, urljoin } from "./lib/url"; import { raise } from "./lib/utils"; -import type { RoomPermissions } from "./permissions"; +import type { RoomPermissions, RoomPermissionsResource } from "./permissions"; import type { ContextualPromptContext, ContextualPromptResponse, @@ -45,6 +45,7 @@ import type { ThreadDataPlain, ThreadDeleteInfo, ThreadDeleteInfoPlain, + ThreadVisibility, } from "./protocol/Comments"; import type { GroupData, GroupDataPlain } from "./protocol/Groups"; import type { @@ -78,6 +79,7 @@ export interface RoomHttpApi { cursor?: string; query?: { resolved?: boolean; + visibility?: ThreadVisibility; subscribed?: boolean; metadata?: Partial>; }; @@ -141,6 +143,7 @@ export interface RoomHttpApi { roomId: string; threadId?: string; commentId?: string; + visibility?: ThreadVisibility; metadata: TM | undefined; commentMetadata: CM | undefined; body: CommentBody; @@ -159,6 +162,7 @@ export interface RoomHttpApi { }: { roomId: string; threadId: string; + visibility?: ThreadVisibility; }): Promise; editThreadMetadata({ @@ -169,6 +173,7 @@ export interface RoomHttpApi { roomId: string; metadata: Patchable; threadId: string; + visibility?: ThreadVisibility; }): Promise; editCommentMetadata({ @@ -181,6 +186,7 @@ export interface RoomHttpApi { threadId: string; commentId: string; metadata: Patchable; + visibility?: ThreadVisibility; }): Promise; createComment({ @@ -197,6 +203,7 @@ export interface RoomHttpApi { body: CommentBody; metadata?: CM; attachmentIds?: string[]; + visibility?: ThreadVisibility; }): Promise>; editComment({ @@ -213,6 +220,7 @@ export interface RoomHttpApi { body: CommentBody; attachmentIds?: string[]; metadata?: Patchable; + visibility?: ThreadVisibility; }): Promise>; deleteComment({ @@ -223,6 +231,7 @@ export interface RoomHttpApi { roomId: string; threadId: string; commentId: string; + visibility?: ThreadVisibility; }): Promise; addReaction({ @@ -235,6 +244,7 @@ export interface RoomHttpApi { threadId: string; commentId: string; emoji: string; + visibility?: ThreadVisibility; }): Promise; removeReaction({ @@ -247,6 +257,7 @@ export interface RoomHttpApi { threadId: string; commentId: string; emoji: string; + visibility?: ThreadVisibility; }): Promise; markThreadAsResolved({ @@ -255,6 +266,7 @@ export interface RoomHttpApi { }: { roomId: string; threadId: string; + visibility?: ThreadVisibility; }): Promise; markThreadAsUnresolved({ @@ -263,6 +275,7 @@ export interface RoomHttpApi { }: { roomId: string; threadId: string; + visibility?: ThreadVisibility; }): Promise; subscribeToThread({ @@ -487,6 +500,7 @@ export interface LiveblocksHttpApi< cursor?: string; query?: { resolved?: boolean; + visibility?: ThreadVisibility; metadata?: Partial>; }; }): Promise<{ @@ -523,6 +537,20 @@ export interface LiveblocksHttpApi< getGroup(groupId: string): Promise; } +function commentsResourceForVisibility( + visibility: ThreadVisibility | undefined +): RoomPermissionsResource { + if (visibility === "private") { + return "comments:private"; + } + + if (visibility === "public") { + return "comments:public"; + } + + return "comments"; +} + export function createApiClient< TM extends BaseMetadata, CM extends BaseMetadata, @@ -596,6 +624,7 @@ export function createApiClient< cursor?: string; query?: { resolved?: boolean; + visibility?: ThreadVisibility; subscribed?: boolean; metadata?: Partial>; }; @@ -625,7 +654,7 @@ export function createApiClient< url`/v2/c/rooms/${options.roomId}/threads`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.query?.visibility), access: "read", }), { @@ -713,6 +742,7 @@ export function createApiClient< roomId: string; threadId?: string; commentId?: string; + visibility?: ThreadVisibility; metadata: TM | undefined; body: CommentBody; commentMetadata?: CM; @@ -725,11 +755,12 @@ export function createApiClient< url`/v2/c/rooms/${options.roomId}/threads`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility ?? "public"), access: "write", }), { id: threadId, + visibility: options.visibility, comment: { id: commentId, body: options.body, @@ -743,12 +774,16 @@ export function createApiClient< return convertToThreadData(thread); } - async function deleteThread(options: { roomId: string; threadId: string }) { + async function deleteThread(options: { + roomId: string; + threadId: string; + visibility?: ThreadVisibility; + }) { await httpClient.delete( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }) ); @@ -797,12 +832,13 @@ export function createApiClient< roomId: string; metadata: Patchable; threadId: string; + visibility?: ThreadVisibility; }) { return await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/metadata`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }), options.metadata @@ -814,12 +850,13 @@ export function createApiClient< threadId: string; commentId: string; metadata: Patchable; + visibility?: ThreadVisibility; }) { return await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/metadata`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }), options.metadata @@ -833,13 +870,14 @@ export function createApiClient< body: CommentBody; metadata?: CM; attachmentIds?: string[]; + visibility?: ThreadVisibility; }) { const commentId = options.commentId ?? createCommentId(); const comment = await httpClient.post>( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }), { @@ -859,12 +897,13 @@ export function createApiClient< body: CommentBody; attachmentIds?: string[]; metadata?: Patchable; + visibility?: ThreadVisibility; }) { const comment = await httpClient.post>( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }), { @@ -881,12 +920,13 @@ export function createApiClient< roomId: string; threadId: string; commentId: string; + visibility?: ThreadVisibility; }) { await httpClient.delete( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }) ); @@ -897,12 +937,13 @@ export function createApiClient< threadId: string; commentId: string; emoji: string; + visibility?: ThreadVisibility; }) { const reaction = await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/reactions`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }), { emoji: options.emoji } @@ -916,12 +957,13 @@ export function createApiClient< threadId: string; commentId: string; emoji: string; + visibility?: ThreadVisibility; }) { await httpClient.delete>( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/comments/${options.commentId}/reactions/${options.emoji}`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }) ); @@ -930,12 +972,13 @@ export function createApiClient< async function markThreadAsResolved(options: { roomId: string; threadId: string; + visibility?: ThreadVisibility; }) { await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/mark-as-resolved`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }) ); @@ -944,12 +987,13 @@ export function createApiClient< async function markThreadAsUnresolved(options: { roomId: string; threadId: string; + visibility?: ThreadVisibility; }) { await httpClient.post( url`/v2/c/rooms/${options.roomId}/threads/${options.threadId}/mark-as-unresolved`, await authManager.getAuthValue({ roomId: options.roomId, - resource: "comments", + resource: commentsResourceForVisibility(options.visibility), access: "write", }) ); @@ -1697,6 +1741,7 @@ export function createApiClient< cursor?: string; query?: { resolved?: boolean; + visibility?: ThreadVisibility; metadata?: Partial>; }; }) { diff --git a/packages/liveblocks-core/src/auth-manager.ts b/packages/liveblocks-core/src/auth-manager.ts index 6e53f5b7034..220537d4a7b 100644 --- a/packages/liveblocks-core/src/auth-manager.ts +++ b/packages/liveblocks-core/src/auth-manager.ts @@ -25,9 +25,11 @@ export type AuthValue = | { type: "secret"; token: ParsedAuthToken } | { type: "public"; publicApiKey: string }; +type RoomAuthResource = Exclude; + export type AuthRequest = Relax< | { - resource: Exclude; + resource: RoomAuthResource; roomId: string; access: RequiredAccessLevel; } @@ -277,10 +279,11 @@ function cachedTokenSatisfiesRequest( request.roomId ); - return ( - matrix !== undefined && - hasPermissionAccess(matrix, request.resource, request.access) - ); + if (matrix === undefined) { + return false; + } + + return hasPermissionAccess(matrix, request.resource, request.access); } function prepareAuthentication( diff --git a/packages/liveblocks-core/src/index.ts b/packages/liveblocks-core/src/index.ts index a5fe0710b43..9fec8239ef9 100644 --- a/packages/liveblocks-core/src/index.ts +++ b/packages/liveblocks-core/src/index.ts @@ -264,6 +264,7 @@ export type { ThreadData, ThreadDataPlain, ThreadDataWithDeleteInfo, + ThreadVisibility, } from "./protocol/Comments"; export type { ThreadDeleteInfo } from "./protocol/Comments"; export type { Feed, FeedMessage } from "./protocol/Feeds"; diff --git a/packages/liveblocks-core/src/permissions.ts b/packages/liveblocks-core/src/permissions.ts index 3bfddcab530..0498ac8f9ec 100644 --- a/packages/liveblocks-core/src/permissions.ts +++ b/packages/liveblocks-core/src/permissions.ts @@ -24,6 +24,12 @@ export const Permission = { CommentsWrite: "comments:write", CommentsRead: "comments:read", CommentsNone: "comments:none", + CommentsPublicWrite: "comments:public:write", + CommentsPublicRead: "comments:public:read", + CommentsPublicNone: "comments:public:none", + CommentsPrivateWrite: "comments:private:write", + CommentsPrivateRead: "comments:private:read", + CommentsPrivateNone: "comments:private:none", /** * Feeds @@ -50,43 +56,38 @@ export type PermissionMatrix = { room: AccessLevel; storage: AccessLevel; comments: AccessLevel; + "comments:public": AccessLevel; + "comments:private": AccessLevel; feeds: AccessLevel; personal: AccessLevel; }; export type PermissionResources = keyof PermissionMatrix; -const basePermissionScopes = new Set([ - Permission.Read, - Permission.Write, - Permission.RoomRead, - Permission.RoomWrite, -]); - -type ResolvedPermissionScopes = { - hasDefaultPermission: boolean; - baseAccess: AccessLevel; - matrix: Partial; -}; - -export type RoomPatternPermissions = { - pattern: string; - scopes: RoomPermissions; -}; - export type RoomPermissions = Permission[]; export type RoomAccesses = Record; export type UpdateRoomAccesses = Record; -type RoomPermissionsResource = Exclude< +export type RoomPermissionsResource = Exclude< PermissionResources, "room" | "personal" >; +type PermissionScopeResource = Exclude; + +type ExplicitPermissionMatrix = Partial< + Record +>; + +export type RoomPatternPermissions = { + pattern: string; + scopes: RoomPermissions; +}; + type ResourcePermissionsMap = Record< - PermissionResources, + PermissionScopeResource, Partial> >; @@ -101,9 +102,6 @@ const PERMISSIONS_BY_RESOURCE: ResourcePermissionsMap = { read: [Permission.Read, Permission.RoomRead], write: [Permission.Write, Permission.RoomWrite], }, - personal: { - write: [], - }, storage: { write: [Permission.StorageWrite], read: [Permission.StorageRead], @@ -114,6 +112,16 @@ const PERMISSIONS_BY_RESOURCE: ResourcePermissionsMap = { read: [Permission.CommentsRead], none: [Permission.CommentsNone], }, + "comments:public": { + write: [Permission.CommentsPublicWrite], + read: [Permission.CommentsPublicRead], + none: [Permission.CommentsPublicNone], + }, + "comments:private": { + write: [Permission.CommentsPrivateWrite], + read: [Permission.CommentsPrivateRead], + none: [Permission.CommentsPrivateNone], + }, feeds: { write: [Permission.FeedsWrite], read: [Permission.FeedsRead], @@ -125,17 +133,33 @@ const NO_PERMISSION_MATRIX: PermissionMatrix = { room: "none", storage: "none", comments: "none", + "comments:public": "none", + "comments:private": "none", feeds: "none", personal: "none", }; -const BASE_PERMISSION_RESOURCE = "room" satisfies PermissionResources; +const BASE_PERMISSION_RESOURCE = "room" satisfies PermissionScopeResource; const ROOM_PERMISSION_RESOURCES = [ "storage", "comments", + "comments:public", + "comments:private", "feeds", -] as const satisfies RoomPermissionsResource[]; +] as const satisfies readonly RoomPermissionsResource[]; + +const COMMENT_VISIBILITY_RESOURCES = [ + "comments:public", + "comments:private", +] as const satisfies readonly RoomPermissionsResource[]; + +const basePermissionScopes = new Set([ + Permission.Read, + Permission.Write, + Permission.RoomRead, + Permission.RoomWrite, +]); const VALID_PERMISSIONS = new Set(Object.values(Permission)); @@ -145,10 +169,9 @@ function isPermission(permission: string): permission is Permission { function resolveResourceAccess( scopes: RoomPermissions, - resource: RoomPermissionsResource + resource: PermissionScopeResource ): AccessLevel | undefined { - const permissions: Partial> = - PERMISSIONS_BY_RESOURCE[resource]; + const permissions = PERMISSIONS_BY_RESOURCE[resource]; let resourceAccess: AccessLevel | undefined; for (const access of ACCESS_LEVELS) { @@ -164,58 +187,53 @@ function resolveResourceAccess( return resourceAccess; } -function permissionMatrixFromResolvedScopes( - resolved: ResolvedPermissionScopes -): PermissionMatrix { - if (!resolved.hasDefaultPermission) { - return { ...NO_PERMISSION_MATRIX }; - } +function explicitPermissionMatrixFromScopes( + scopes: RoomPermissions +): ExplicitPermissionMatrix { + const matrix: ExplicitPermissionMatrix = {}; - const matrix: PermissionMatrix = { - ...NO_PERMISSION_MATRIX, - [BASE_PERMISSION_RESOURCE]: resolved.baseAccess, - personal: "write", - }; + const baseAccess = resolveResourceAccess(scopes, BASE_PERMISSION_RESOURCE); + if (baseAccess !== undefined) { + matrix.room = baseAccess; + } for (const resource of ROOM_PERMISSION_RESOURCES) { - matrix[resource] = resolved.matrix[resource] ?? resolved.baseAccess; + const access = resolveResourceAccess(scopes, resource); + if (access !== undefined) { + matrix[resource] = access; + } } return matrix; } -export function permissionMatrixFromScopes( - scopes: RoomPermissions +function permissionMatrixFromExplicitPermissions( + explicitMatrix: ExplicitPermissionMatrix ): PermissionMatrix { - return permissionMatrixFromResolvedScopes(resolvePermissionScopes(scopes)); -} + const baseAccess = explicitMatrix.room; + if (baseAccess === undefined) { + return { ...NO_PERMISSION_MATRIX }; + } -function resolvePermissionScopes( - scopes: RoomPermissions -): ResolvedPermissionScopes { - const hasDefaultPermission = - scopes.includes(Permission.Write) || - scopes.includes(Permission.Read) || - scopes.includes(Permission.RoomWrite) || - scopes.includes(Permission.RoomRead); - - const baseAccess: AccessLevel = - scopes.includes(Permission.Write) || scopes.includes(Permission.RoomWrite) - ? "write" - : scopes.includes(Permission.Read) || scopes.includes(Permission.RoomRead) - ? "read" - : "none"; - - const matrix: Partial = {}; + const commentsAccess = explicitMatrix.comments ?? baseAccess; - for (const resource of ROOM_PERMISSION_RESOURCES) { - const access = resolveResourceAccess(scopes, resource); - if (access !== undefined) { - matrix[resource] = access; - } - } + return { + room: baseAccess, + storage: explicitMatrix.storage ?? baseAccess, + comments: commentsAccess, + "comments:public": explicitMatrix["comments:public"] ?? commentsAccess, + "comments:private": explicitMatrix["comments:private"] ?? commentsAccess, + feeds: explicitMatrix.feeds ?? baseAccess, + personal: "write", + }; +} - return { hasDefaultPermission, baseAccess, matrix }; +export function permissionMatrixFromScopes( + scopes: RoomPermissions +): PermissionMatrix { + return permissionMatrixFromExplicitPermissions( + explicitPermissionMatrixFromScopes(scopes) + ); } export function hasPermissionAccess( @@ -223,8 +241,19 @@ export function hasPermissionAccess( resource: PermissionResources, requiredAccess: RequiredAccessLevel ): boolean { + const requiredRank = ACCESS_LEVEL_RANKS[requiredAccess]; + + if (resource === "comments") { + const commentsRank = Math.max( + ACCESS_LEVEL_RANKS[matrix.comments ?? "none"], + ACCESS_LEVEL_RANKS[matrix["comments:public"] ?? "none"], + ACCESS_LEVEL_RANKS[matrix["comments:private"] ?? "none"] + ); + return commentsRank >= requiredRank; + } + const access = matrix[resource] ?? "none"; - return ACCESS_LEVEL_RANKS[access] >= ACCESS_LEVEL_RANKS[requiredAccess]; + return ACCESS_LEVEL_RANKS[access] >= requiredRank; } export function resolveRoomPermissionMatrix( @@ -239,45 +268,38 @@ export function resolveRoomPermissionMatrix( return undefined; } - let hasDefaultPermission = false; - let baseAccess: AccessLevel = "none"; - const explicitMatrix: Partial = {}; - const explicitSpecificity: Partial> = {}; + const matrix: ExplicitPermissionMatrix = {}; + const specificityByResource: Partial< + Record + > = {}; for (const entry of matchedPermissions) { - const resolved = resolvePermissionScopes(entry.scopes); + const explicitMatrix = explicitPermissionMatrixFromScopes(entry.scopes); const specificity = roomPatternSpecificity(entry.pattern); - if (resolved.hasDefaultPermission) { - hasDefaultPermission = true; + if (explicitMatrix.room !== undefined) { // Base access is additive across all matching patterns (highest wins), // unlike resource-specific overrides which use most-specific-wins. - baseAccess = strongestAccess(baseAccess, resolved.baseAccess); + matrix.room = strongestAccess(matrix.room ?? "none", explicitMatrix.room); } for (const resource of ROOM_PERMISSION_RESOURCES) { - const access = resolved.matrix[resource]; - if (access !== undefined) { - const currentSpecificity = explicitSpecificity[resource] ?? -1; - - if (specificity > currentSpecificity) { - explicitMatrix[resource] = access; - explicitSpecificity[resource] = specificity; - } else if (specificity === currentSpecificity) { - explicitMatrix[resource] = strongestAccess( - explicitMatrix[resource] ?? "none", - access - ); - } + const access = explicitAccessForResource(explicitMatrix, resource); + if (access === undefined) { + continue; + } + + const currentSpecificity = specificityByResource[resource] ?? -1; + if (specificity > currentSpecificity) { + matrix[resource] = access; + specificityByResource[resource] = specificity; + } else if (specificity === currentSpecificity) { + matrix[resource] = strongestAccess(matrix[resource] ?? "none", access); } } } - return permissionMatrixFromResolvedScopes({ - hasDefaultPermission, - baseAccess, - matrix: explicitMatrix, - }); + return permissionMatrixFromExplicitPermissions(matrix); } export function normalizeRoomPermissions( @@ -339,13 +361,25 @@ export function permissionMatrixToScopes( scopes.push(permissionForAccessLevel(BASE_PERMISSION_RESOURCE, baseAccess)); } - for (const resource of ROOM_PERMISSION_RESOURCES) { - const access = matrix[resource]; - if (access !== baseAccess) { - scopes.push(permissionForAccessLevel(resource, access)); + if (matrix.storage !== baseAccess) { + scopes.push(permissionForAccessLevel("storage", matrix.storage)); + } + + const commentsAccess = matrix.comments; + if (commentsAccess !== baseAccess) { + scopes.push(permissionForAccessLevel("comments", commentsAccess)); + } + + for (const resource of COMMENT_VISIBILITY_RESOURCES) { + if (matrix[resource] !== commentsAccess) { + scopes.push(permissionForAccessLevel(resource, matrix[resource])); } } + if (matrix.feeds !== baseAccess) { + scopes.push(permissionForAccessLevel("feeds", matrix.feeds)); + } + return scopes; } @@ -366,58 +400,47 @@ export function mergeRoomPermissionScopes({ }): RoomPermissions { // Ordered from lowest to highest priority const sources = [ - resolvePermissionScopes(defaultAccesses), - mergeResolvedScopesByHighestAccess( - groupsAccesses.map(resolvePermissionScopes) + explicitPermissionMatrixFromScopes(defaultAccesses), + mergeExplicitPermissionMatricesByHighestAccess( + groupsAccesses.map(explicitPermissionMatrixFromScopes) ), - resolvePermissionScopes(userAccesses), + explicitPermissionMatrixFromScopes(userAccesses), ]; - const merged: ResolvedPermissionScopes = { - hasDefaultPermission: false, - baseAccess: "none", - matrix: {}, - }; + const merged: ExplicitPermissionMatrix = {}; for (const source of sources) { - if (source.hasDefaultPermission) { - merged.hasDefaultPermission = true; - merged.baseAccess = source.baseAccess; + if (source.room !== undefined) { + merged.room = source.room; } for (const resource of ROOM_PERMISSION_RESOURCES) { - const access = source.matrix[resource]; + const access = explicitAccessForResource(source, resource); if (access !== undefined) { - merged.matrix[resource] = access; + merged[resource] = access; } } } - return permissionMatrixToScopes(permissionMatrixFromResolvedScopes(merged)); + return permissionMatrixToScopes( + permissionMatrixFromExplicitPermissions(merged) + ); } -function mergeResolvedScopesByHighestAccess( - sources: ResolvedPermissionScopes[] -): ResolvedPermissionScopes { - const merged: ResolvedPermissionScopes = { - hasDefaultPermission: false, - baseAccess: "none", - matrix: {}, - }; +function mergeExplicitPermissionMatricesByHighestAccess( + sources: ExplicitPermissionMatrix[] +): ExplicitPermissionMatrix { + const merged: ExplicitPermissionMatrix = {}; for (const source of sources) { - if (source.hasDefaultPermission) { - merged.hasDefaultPermission = true; - merged.baseAccess = strongestAccess(merged.baseAccess, source.baseAccess); + if (source.room !== undefined) { + merged.room = strongestAccess(merged.room ?? "none", source.room); } for (const resource of ROOM_PERMISSION_RESOURCES) { - const access = source.matrix[resource]; + const access = explicitAccessForResource(source, resource); if (access !== undefined) { - merged.matrix[resource] = strongestAccess( - merged.matrix[resource] ?? "none", - access - ); + merged[resource] = strongestAccess(merged[resource] ?? "none", access); } } } @@ -425,20 +448,30 @@ function mergeResolvedScopesByHighestAccess( return merged; } +function explicitAccessForResource( + source: ExplicitPermissionMatrix, + resource: RoomPermissionsResource +): AccessLevel | undefined { + return ( + source[resource] ?? + (isCommentVisibilityResource(resource) ? source.comments : undefined) + ); +} + function permissionForAccessLevel( - resource: PermissionResources, + resource: PermissionScopeResource, access: AccessLevel, field: string = resource ): Permission { - const levels: Partial> = - PERMISSIONS_BY_RESOURCE[resource]; - const permissions = levels[access]; - if (permissions === undefined || permissions.length === 0) { - throw new Error( - `Invalid permission level for ${field}: ${JSON.stringify(access) ?? String(access)}` - ); + const permissions = PERMISSIONS_BY_RESOURCE[resource][access]; + const permission = permissions?.[0]; + if (permission !== undefined) { + return permission; } - return permissions[0]; + + throw new Error( + `Invalid permission level for ${field}: ${JSON.stringify(access) ?? String(access)}` + ); } function strongestAccess(left: AccessLevel, right: AccessLevel): AccessLevel { @@ -493,7 +526,7 @@ export function validatePermissionsSet( continue; } - const feature = scope.slice(0, scope.indexOf(":")); + const feature = permissionFeature(scope); if (seenFeatures.has(feature)) { return `Permissions can include at most one scope per feature, got multiple "${feature}" scopes`; } @@ -502,3 +535,16 @@ export function validatePermissionsSet( return true; } + +function permissionFeature(scope: string): string { + const accessSeparatorIndex = scope.lastIndexOf(":"); + return accessSeparatorIndex === -1 + ? scope + : scope.slice(0, accessSeparatorIndex); +} + +function isCommentVisibilityResource( + resource: RoomPermissionsResource +): resource is (typeof COMMENT_VISIBILITY_RESOURCES)[number] { + return resource.startsWith("comments:"); +} diff --git a/packages/liveblocks-core/src/protocol/Comments.ts b/packages/liveblocks-core/src/protocol/Comments.ts index 23392cb5e61..c7c98d12089 100644 --- a/packages/liveblocks-core/src/protocol/Comments.ts +++ b/packages/liveblocks-core/src/protocol/Comments.ts @@ -172,6 +172,8 @@ export type SearchCommentsResult = { content: string; }; +export type ThreadVisibility = "public" | "private"; + /** * Represents a thread of comments. */ @@ -187,6 +189,7 @@ export type ThreadData< comments: CommentData[]; metadata: TM; resolved: boolean; + visibility: ThreadVisibility; }; export interface ThreadDataWithDeleteInfo< diff --git a/packages/liveblocks-core/src/room.ts b/packages/liveblocks-core/src/room.ts index ce8ad1d634f..a9d6ca645ca 100644 --- a/packages/liveblocks-core/src/room.ts +++ b/packages/liveblocks-core/src/room.ts @@ -90,6 +90,7 @@ import type { QueryMetadata, ThreadData, ThreadDeleteInfo, + ThreadVisibility, } from "./protocol/Comments"; import type { Feed, FeedMessage } from "./protocol/Feeds"; import type { @@ -535,6 +536,7 @@ export type GetThreadsOptions = { cursor?: string; query?: { resolved?: boolean; + visibility?: ThreadVisibility; subscribed?: boolean; metadata?: Partial>; }; @@ -986,6 +988,7 @@ export type Room< createThread(options: { threadId?: string; commentId?: string; + visibility?: ThreadVisibility; metadata: TM | undefined; body: CommentBody; commentMetadata?: CM; @@ -1248,6 +1251,8 @@ export type PrivateRoomApi = { // For reporting editor metadata reportTextEditor(editor: TextEditorType, rootKey: string): Promise; + getPermissionMatrix(): PermissionMatrix | undefined; + createTextMention(mentionId: string, mention: MentionData): Promise; deleteTextMention(mentionId: string): Promise; listTextVersions(): Promise<{ @@ -3565,6 +3570,7 @@ export function createRoom< roomId: string; threadId?: string; commentId?: string; + visibility?: ThreadVisibility; metadata: TM | undefined; commentMetadata: CM | undefined; body: CommentBody; @@ -3574,6 +3580,7 @@ export function createRoom< roomId, threadId: options.threadId, commentId: options.commentId, + visibility: options.visibility, metadata: options.metadata, body: options.body, commentMetadata: options.commentMetadata, @@ -3792,6 +3799,8 @@ export function createRoom< // send metadata when using a text editor reportTextEditor, + getPermissionMatrix: () => + context.dynamicSessionInfoSig.get()?.permissionMatrix, // create a text mention when using a text editor createTextMention, // delete a text mention when using a text editor diff --git a/packages/liveblocks-core/src/types/LiveblocksError.ts b/packages/liveblocks-core/src/types/LiveblocksError.ts index 0fc0bcbeca4..0493dd288da 100644 --- a/packages/liveblocks-core/src/types/LiveblocksError.ts +++ b/packages/liveblocks-core/src/types/LiveblocksError.ts @@ -1,6 +1,10 @@ import { assertNever } from "../lib/assert"; import type { Relax } from "../lib/Relax"; -import type { BaseMetadata, CommentBody } from "../protocol/Comments"; +import type { + BaseMetadata, + CommentBody, + ThreadVisibility, +} from "../protocol/Comments"; import type { Patchable } from "./Patchable"; // All possible error originating from using Presence, Storage, or Yjs @@ -36,6 +40,7 @@ type CommentsOrNotificationsErrorContext = threadId: string; commentId: string; body: CommentBody; + visibility: ThreadVisibility; metadata: BaseMetadata; commentMetadata: BaseMetadata; } diff --git a/packages/liveblocks-emails/src/__tests__/_helpers.ts b/packages/liveblocks-emails/src/__tests__/_helpers.ts index 676a00b6fa7..22ed1dba766 100644 --- a/packages/liveblocks-emails/src/__tests__/_helpers.ts +++ b/packages/liveblocks-emails/src/__tests__/_helpers.ts @@ -303,6 +303,7 @@ export const makeThread = ({ roomId: ROOM_ID_TEST, metadata: {}, resolved: false, + visibility: "public", createdAt: at, updatedAt: at, comments, diff --git a/packages/liveblocks-node/src/__tests__/Session.test.ts b/packages/liveblocks-node/src/__tests__/Session.test.ts index 61c813d1631..86cfe60ac4f 100644 --- a/packages/liveblocks-node/src/__tests__/Session.test.ts +++ b/packages/liveblocks-node/src/__tests__/Session.test.ts @@ -5,7 +5,8 @@ import { Liveblocks } from "../client"; const P1 = "*:read"; const P2 = "*:write"; const P3 = "comments:read"; -// const P4 = "comments:write"; +const P4 = "comments:public:write"; +const P5 = "comments:private:none"; function makeSession(options?: { secret?: string; @@ -180,6 +181,14 @@ describe("authorization (new API)", () => { }); }); + test("accepts scoped comments permissions", () => { + expect( + makeSession().allow("foo", [P1, P4, P5]).serializePermissions() + ).toEqual({ + foo: [P1, P4, P5], + }); + }); + test("permissions are preserved when adding defaults and resource-specific values", () => { expect( makeSession() diff --git a/packages/liveblocks-node/src/__tests__/client.test.ts b/packages/liveblocks-node/src/__tests__/client.test.ts index 854a42f85ca..dd7e3829b95 100644 --- a/packages/liveblocks-node/src/__tests__/client.test.ts +++ b/packages/liveblocks-node/src/__tests__/client.test.ts @@ -91,6 +91,7 @@ describe("client", () => { updatedAt: new Date("2022-07-13T14:32:50.697Z"), comments: [comment], resolved: false, + visibility: "public", }; const reaction: CommentUserReaction = { @@ -942,6 +943,7 @@ describe("client", () => { metadata: { color: "blue", }, + visibility: "private", }; server.use( @@ -1199,9 +1201,9 @@ describe("client", () => { }); }); - test("should return a filtered list of threads when a query parameter is used for getThreads with a metadata object", async () => { + test("should return a filtered list of threads when a query parameter is used for getThreads with an object", async () => { const expectedQuery = - "metadata['status']:'open' metadata['priority']:3 metadata['organization']^'liveblocks:'"; + "resolved:false visibility:'private' metadata['status']:'open' metadata['priority']:3 metadata['organization']^'liveblocks:'"; server.use( http.get( @@ -1227,6 +1229,8 @@ describe("client", () => { startsWith: "liveblocks:", }, }, + resolved: false, + visibility: "private", }, }) ).resolves.toEqual({ diff --git a/packages/liveblocks-node/src/client.ts b/packages/liveblocks-node/src/client.ts index c07ebca7193..998d5cb7276 100644 --- a/packages/liveblocks-node/src/client.ts +++ b/packages/liveblocks-node/src/client.ts @@ -53,6 +53,7 @@ import type { SubscriptionDataPlain, ThreadData, ThreadDataPlain, + ThreadVisibility, ToJson, UpdateRoomAccesses, URLSafeString, @@ -161,6 +162,7 @@ export type CreateThreadOptions< > = { roomId: string; data: { + visibility?: ThreadVisibility; comment: { userId: string; createdAt?: Date; @@ -1648,7 +1650,7 @@ export class Liveblocks { * Gets all the threads in a room. * * @param params.roomId The room ID to get the threads from. - * @param params.query The query to filter threads by. It is based on our query language and can filter by metadata. + * @param params.query The query to filter threads by. It is based on our query language and can filter by visibility, metadata, and resolved status. * @param options.signal (optional) An abort signal to cancel the request. * @returns A list of threads. */ @@ -1661,7 +1663,7 @@ export class Liveblocks { * @example * ``` * { - * query: "metadata['organization']^'liveblocks:' AND metadata['status']:'open' AND metadata['pinned']:false AND metadata['priority']:3 AND resolved:true" + * query: "metadata['organization']^'liveblocks:' AND metadata['status']:'open' AND metadata['pinned']:false AND metadata['priority']:3 AND resolved:true AND visibility:'private'" * } * ``` * @example @@ -1676,7 +1678,8 @@ export class Liveblocks { * startsWith: "liveblocks:" * } * }, - * resolved: true + * resolved: true, + * visibility: "private" * } * } * ``` @@ -1686,6 +1689,7 @@ export class Liveblocks { | { metadata?: Partial>; resolved?: boolean; + visibility?: ThreadVisibility; }; }, options?: RequestOptions diff --git a/packages/liveblocks-node/test-d/augmentation.test-d.ts b/packages/liveblocks-node/test-d/augmentation.test-d.ts index bba27f920ff..98b52dc7159 100644 --- a/packages/liveblocks-node/test-d/augmentation.test-d.ts +++ b/packages/liveblocks-node/test-d/augmentation.test-d.ts @@ -266,6 +266,7 @@ describe("Liveblocks client with Liveblocks augmentation", () => { metadata: { priority: 1 }, }, metadata: { color: "red" }, + visibility: "private", }, }); diff --git a/packages/liveblocks-node/test-d/no-augmentation.test-d.ts b/packages/liveblocks-node/test-d/no-augmentation.test-d.ts index 77447fd9714..5f7846bc60f 100644 --- a/packages/liveblocks-node/test-d/no-augmentation.test-d.ts +++ b/packages/liveblocks-node/test-d/no-augmentation.test-d.ts @@ -186,6 +186,7 @@ describe("Liveblocks client without Liveblocks augmentation", () => { body: { version: 1, content: [] }, }, metadata: { color: "red" }, + visibility: "private", }, }); diff --git a/packages/liveblocks-python/README.md b/packages/liveblocks-python/README.md index 2066e21680f..c437eb84604 100644 --- a/packages/liveblocks-python/README.md +++ b/packages/liveblocks-python/README.md @@ -86,10 +86,10 @@ print(result) #### `create_room` This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided. -- `defaultAccesses` could be `[]` or `["*:write"]` (private or public). +- `defaultAccesses` is the default room permission list, for example `[]`, `["*:read"]`, `["*:write"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. -- `usersAccesses` could be `[]` or `["*:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. -- `groupsAccesses` are optional fields. +- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. +- `groupsAccesses` contains group-specific permission lists and is optional. **Example** @@ -151,10 +151,10 @@ Setting a property to `null` means to delete this property. For example, if you }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. -- `defaultAccesses` could be `[]` or `["*:write"]` (private or public). +- `defaultAccesses` is the default room permission list, for example `[]`, `["*:read"]`, `["*:write"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. -- `usersAccesses` could be `[]` or `["*:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. -- `groupsAccesses` could be `[]` or `["*:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. +- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. +- `groupsAccesses` contains group-specific permission lists and is optional. **Example** ```python @@ -231,10 +231,10 @@ Setting a property to `null` means to delete this property. For example, if you }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. -- `defaultAccesses` could be `[]` or `["*:write"]` (private or public). +- `defaultAccesses` is the default room permission list, for example `[]`, `["*:read"]`, `["*:write"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. -- `usersAccesses` could be `[]` or `["*:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. -- `groupsAccesses` could be `[]` or `["*:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. +- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. +- `groupsAccesses` contains group-specific permission lists and is optional. **Example** ```python @@ -663,7 +663,7 @@ print(result) | Name | Type | Required | Description | |------|------|----------|-------------| | `room_id` | `str` | Yes | ID of the room | -| `query` | `str \| Unset` | No | Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). | +| `query` | `str \| Unset` | No | Query to filter threads. You can filter by `metadata`, `resolved`, and `visibility`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true AND visibility:"private"`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). | --- @@ -697,6 +697,7 @@ result = client.create_thread( body=CreateThreadRequestBody( comment=..., # metadata=..., + # visibility=..., ), ) print(result) diff --git a/packages/liveblocks-python/README.mdx b/packages/liveblocks-python/README.mdx index 8cd1a7baacd..ac2db2d8ff0 100644 --- a/packages/liveblocks-python/README.mdx +++ b/packages/liveblocks-python/README.mdx @@ -110,10 +110,10 @@ print(result) ### create_room This endpoint creates a new room. `id` and `defaultAccesses` are required. When provided with a `?idempotent` query argument, will not return a 409 when the room already exists, but instead return the existing room as-is. Corresponds to [`liveblocks.createRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or-create-rooms-roomId) when `?idempotent` is provided. -- `defaultAccesses` could be `[]` or `["*:write"]` (private or public). +- `defaultAccesses` is the default room permission list, for example `[]`, `["*:read"]`, `["*:write"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. -- `usersAccesses` could be `[]` or `["*:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. -- `groupsAccesses` are optional fields. +- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. +- `groupsAccesses` contains group-specific permission lists and is optional. ```python @@ -184,10 +184,10 @@ Setting a property to `null` means to delete this property. For example, if you }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. -- `defaultAccesses` could be `[]` or `["*:write"]` (private or public). +- `defaultAccesses` is the default room permission list, for example `[]`, `["*:read"]`, `["*:write"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. -- `usersAccesses` could be `[]` or `["*:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. -- `groupsAccesses` could be `[]` or `["*:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. +- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. +- `groupsAccesses` contains group-specific permission lists and is optional. ```python from liveblocks.models import UpdateRoomRequestBody @@ -276,10 +276,10 @@ Setting a property to `null` means to delete this property. For example, if you }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. -- `defaultAccesses` could be `[]` or `["*:write"]` (private or public). +- `defaultAccesses` is the default room permission list, for example `[]`, `["*:read"]`, `["*:write"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. -- `usersAccesses` could be `[]` or `["*:write"]` for every records. `usersAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. -- `groupsAccesses` could be `[]` or `["*:write"]` for every records. `groupsAccesses` can contain 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. +- `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. +- `groupsAccesses` contains group-specific permission lists and is optional. ```python from liveblocks.models import UpsertRoomRequestBody @@ -841,7 +841,7 @@ print(result) name="query" type="str | Unset" > - Query to filter threads. You can filter by `metadata` and `resolved`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). + Query to filter threads. You can filter by `metadata`, `resolved`, and `visibility`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true AND visibility:"private"`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). @@ -874,6 +874,7 @@ result = client.create_thread( body=CreateThreadRequestBody( comment=..., # metadata=..., + # visibility=..., ), ) print(result) diff --git a/packages/liveblocks-python/liveblocks/client.py b/packages/liveblocks-python/liveblocks/client.py index cf846533b6b..13bf3b81e7c 100644 --- a/packages/liveblocks-python/liveblocks/client.py +++ b/packages/liveblocks-python/liveblocks/client.py @@ -239,13 +239,14 @@ def create_room( reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or- create-rooms-roomId) when `?idempotent` is provided. - - `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). + - `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, + `[\"*:write\"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. - - `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. - - `groupsAccesses` are optional fields. + - `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id + length has a limit of 256 characters. `usersAccesses` is optional field. + - `groupsAccesses` contains group-specific permission lists and is optional. Args: idempotent (bool | Unset): When provided, will not return a 409 when the room already @@ -321,14 +322,14 @@ def update_room( }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. - - `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). + - `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, + `[\"*:write\"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. - - `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. - - `groupsAccesses` could be `[]` or `[\"*:write\"]` for every records. `groupsAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. + - `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id + length has a limit of 256 characters. `usersAccesses` is optional field. + - `groupsAccesses` contains group-specific permission lists and is optional. Args: room_id (str): ID of the room Example: my-room-id. @@ -433,14 +434,14 @@ def upsert_room( }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. - - `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). + - `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, + `[\"*:write\"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. - - `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. - - `groupsAccesses` could be `[]` or `[\"*:write\"]` for every records. `groupsAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. + - `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id + length has a limit of 256 characters. `usersAccesses` is optional field. + - `groupsAccesses` contains group-specific permission lists and is optional. Args: room_id (str): ID of the room Example: my-room-id. @@ -1030,9 +1031,9 @@ def get_threads( Args: room_id (str): ID of the room Example: my-room-id. - query (str | Unset): Query to filter threads. You can filter by `metadata` and `resolved`, - for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`. - Learn more about [filtering threads with query + query (str | Unset): Query to filter threads. You can filter by `metadata`, `resolved`, + and `visibility`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND + resolved:true AND visibility:"private"`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). Example: metadata["color"]:"blue". @@ -3373,13 +3374,14 @@ async def create_room( reference/liveblocks-node#post-rooms), or to [`liveblocks.getOrCreateRoom`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-or- create-rooms-roomId) when `?idempotent` is provided. - - `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). + - `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, + `[\"*:write\"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. - - `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. - - `groupsAccesses` are optional fields. + - `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id + length has a limit of 256 characters. `usersAccesses` is optional field. + - `groupsAccesses` contains group-specific permission lists and is optional. Args: idempotent (bool | Unset): When provided, will not return a 409 when the room already @@ -3455,14 +3457,14 @@ async def update_room( }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. - - `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). + - `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, + `[\"*:write\"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. - - `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. - - `groupsAccesses` could be `[]` or `[\"*:write\"]` for every records. `groupsAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. + - `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id + length has a limit of 256 characters. `usersAccesses` is optional field. + - `groupsAccesses` contains group-specific permission lists and is optional. Args: room_id (str): ID of the room Example: my-room-id. @@ -3567,14 +3569,14 @@ async def upsert_room( }`` `defaultAccesses`, `metadata`, `usersAccesses`, `groupsAccesses` can be updated. - - `defaultAccesses` could be `[]` or `[\"*:write\"]` (private or public). + - `defaultAccesses` is the default room permission list, for example `[]`, `[\"*:read\"]`, + `[\"*:write\"]`, or a more granular permission list. - `metadata` could be key/value as `string` or `string[]`. `metadata` supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 256 characters maximum. `metadata` is optional field. - - `usersAccesses` could be `[]` or `[\"*:write\"]` for every records. `usersAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `usersAccesses` is optional field. - - `groupsAccesses` could be `[]` or `[\"*:write\"]` for every records. `groupsAccesses` can contain - 1000 ids maximum. Id length has a limit of 256 characters. `groupsAccesses` is optional field. + - `usersAccesses` contains user-specific permission lists. It can contain 1000 ids maximum. Id + length has a limit of 256 characters. `usersAccesses` is optional field. + - `groupsAccesses` contains group-specific permission lists and is optional. Args: room_id (str): ID of the room Example: my-room-id. @@ -4164,9 +4166,9 @@ async def get_threads( Args: room_id (str): ID of the room Example: my-room-id. - query (str | Unset): Query to filter threads. You can filter by `metadata` and `resolved`, - for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND resolved:true`. - Learn more about [filtering threads with query + query (str | Unset): Query to filter threads. You can filter by `metadata`, `resolved`, + and `visibility`, for example, `metadata["status"]:"open" AND metadata["color"]:"red" AND + resolved:true AND visibility:"private"`. Learn more about [filtering threads with query language](https://liveblocks.io/docs/guides/how-to-filter-threads-using-query-language). Example: metadata["color"]:"blue". diff --git a/packages/liveblocks-python/liveblocks/models/__init__.py b/packages/liveblocks-python/liveblocks/models/__init__.py index 7c9ed5dddc0..6c498030c30 100644 --- a/packages/liveblocks-python/liveblocks/models/__init__.py +++ b/packages/liveblocks-python/liveblocks/models/__init__.py @@ -54,6 +54,7 @@ from .create_room_request_body_engine import CreateRoomRequestBodyEngine from .create_thread_request_body import CreateThreadRequestBody from .create_thread_request_body_comment import CreateThreadRequestBodyComment +from .create_thread_request_body_visibility import CreateThreadRequestBodyVisibility from .create_web_knowledge_source_request_body import CreateWebKnowledgeSourceRequestBody from .create_web_knowledge_source_request_body_type import CreateWebKnowledgeSourceRequestBodyType from .create_web_knowledge_source_response import CreateWebKnowledgeSourceResponse @@ -144,6 +145,7 @@ from .test_json_patch_operation import TestJsonPatchOperation from .thread import Thread from .thread_metadata import ThreadMetadata +from .thread_visibility import ThreadVisibility from .trigger_inbox_notification_request_body import TriggerInboxNotificationRequestBody from .trigger_inbox_notification_request_body_activity_data import TriggerInboxNotificationRequestBodyActivityData from .unsubscribe_from_thread_request_body import UnsubscribeFromThreadRequestBody @@ -225,6 +227,7 @@ "CreateRoomRequestBodyEngine", "CreateThreadRequestBody", "CreateThreadRequestBodyComment", + "CreateThreadRequestBodyVisibility", "CreateWebKnowledgeSourceRequestBody", "CreateWebKnowledgeSourceRequestBodyType", "CreateWebKnowledgeSourceResponse", @@ -316,6 +319,7 @@ "TestJsonPatchOperation", "Thread", "ThreadMetadata", + "ThreadVisibility", "TriggerInboxNotificationRequestBody", "TriggerInboxNotificationRequestBodyActivityData", "UnsubscribeFromThreadRequestBody", diff --git a/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py b/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py index ff8cfefa9fc..6c7622d9b05 100644 --- a/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py +++ b/packages/liveblocks-python/liveblocks/models/create_thread_request_body.py @@ -6,6 +6,7 @@ from attrs import define as _attrs_define from attrs import field as _attrs_field +from ..models.create_thread_request_body_visibility import CreateThreadRequestBodyVisibility from ..types import UNSET, Unset if TYPE_CHECKING: @@ -20,10 +21,12 @@ class CreateThreadRequestBody: comment (CreateThreadRequestBodyComment): metadata (ThreadMetadata | Unset): Custom metadata attached to a thread. Supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings. + visibility (CreateThreadRequestBodyVisibility | Unset): Default: CreateThreadRequestBodyVisibility.PUBLIC. """ comment: CreateThreadRequestBodyComment metadata: ThreadMetadata | Unset = UNSET + visibility: CreateThreadRequestBodyVisibility | Unset = CreateThreadRequestBodyVisibility.PUBLIC additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) def to_dict(self) -> dict[str, Any]: @@ -33,6 +36,10 @@ def to_dict(self) -> dict[str, Any]: if not isinstance(self.metadata, Unset): metadata = self.metadata.to_dict() + visibility: str | Unset = UNSET + if not isinstance(self.visibility, Unset): + visibility = self.visibility.value + field_dict: dict[str, Any] = {} field_dict.update(self.additional_properties) field_dict.update( @@ -42,6 +49,8 @@ def to_dict(self) -> dict[str, Any]: ) if metadata is not UNSET: field_dict["metadata"] = metadata + if visibility is not UNSET: + field_dict["visibility"] = visibility return field_dict @@ -60,9 +69,17 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: else: metadata = ThreadMetadata.from_dict(_metadata) + _visibility = d.pop("visibility", UNSET) + visibility: CreateThreadRequestBodyVisibility | Unset + if isinstance(_visibility, Unset): + visibility = UNSET + else: + visibility = CreateThreadRequestBodyVisibility(_visibility) + create_thread_request_body = cls( comment=comment, metadata=metadata, + visibility=visibility, ) create_thread_request_body.additional_properties = d diff --git a/packages/liveblocks-python/liveblocks/models/create_thread_request_body_visibility.py b/packages/liveblocks-python/liveblocks/models/create_thread_request_body_visibility.py new file mode 100644 index 00000000000..3163a4e2cfa --- /dev/null +++ b/packages/liveblocks-python/liveblocks/models/create_thread_request_body_visibility.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class CreateThreadRequestBodyVisibility(StrEnum): + PRIVATE = "private" + PUBLIC = "public" diff --git a/packages/liveblocks-python/liveblocks/models/get_threads_response.py b/packages/liveblocks-python/liveblocks/models/get_threads_response.py index eac780933c7..bf7bbb50634 100644 --- a/packages/liveblocks-python/liveblocks/models/get_threads_response.py +++ b/packages/liveblocks-python/liveblocks/models/get_threads_response.py @@ -17,7 +17,7 @@ class GetThreadsResponse: 'threadId': 'th_abc123', 'roomId': 'my-room-id', 'id': 'cm_abc123', 'userId': 'alice', 'createdAt': '2022-07-13T14:32:50.697Z', 'body': {'version': 1, 'content': []}, 'metadata': {}, 'reactions': [], 'attachments': []}], 'createdAt': '2022-07-13T14:32:50.697Z', 'updatedAt': '2022-07-13T14:32:50.697Z', - 'metadata': {}, 'resolved': False}]} + 'metadata': {}, 'resolved': False, 'visibility': 'public'}]} Attributes: data (list[Thread]): diff --git a/packages/liveblocks-python/liveblocks/models/room_permission_item.py b/packages/liveblocks-python/liveblocks/models/room_permission_item.py index 5aa7f4aeec6..d4360475bbc 100644 --- a/packages/liveblocks-python/liveblocks/models/room_permission_item.py +++ b/packages/liveblocks-python/liveblocks/models/room_permission_item.py @@ -3,6 +3,12 @@ class RoomPermissionItem(StrEnum): COMMENTSNONE = "comments:none" + COMMENTSPRIVATENONE = "comments:private:none" + COMMENTSPRIVATEREAD = "comments:private:read" + COMMENTSPRIVATEWRITE = "comments:private:write" + COMMENTSPUBLICNONE = "comments:public:none" + COMMENTSPUBLICREAD = "comments:public:read" + COMMENTSPUBLICWRITE = "comments:public:write" COMMENTSREAD = "comments:read" COMMENTSWRITE = "comments:write" FEEDSNONE = "feeds:none" diff --git a/packages/liveblocks-python/liveblocks/models/thread.py b/packages/liveblocks-python/liveblocks/models/thread.py index bbc9732aa45..6fc9629abba 100644 --- a/packages/liveblocks-python/liveblocks/models/thread.py +++ b/packages/liveblocks-python/liveblocks/models/thread.py @@ -8,6 +8,8 @@ from attrs import field as _attrs_field from dateutil.parser import isoparse +from ..models.thread_visibility import ThreadVisibility + if TYPE_CHECKING: from ..models.comment import Comment from ..models.thread_metadata import ThreadMetadata @@ -21,7 +23,7 @@ class Thread: 'th_abc123', 'roomId': 'my-room-id', 'id': 'cm_abc123', 'userId': 'alice', 'createdAt': '2022-07-13T14:32:50.697Z', 'body': {'version': 1, 'content': []}, 'metadata': {}, 'reactions': [], 'attachments': []}], 'createdAt': '2022-07-13T14:32:50.697Z', 'updatedAt': '2022-07-13T14:32:50.697Z', - 'metadata': {'color': 'blue'}, 'resolved': False} + 'metadata': {'color': 'blue'}, 'resolved': False, 'visibility': 'public'} Attributes: type_ (Literal['thread']): @@ -32,6 +34,7 @@ class Thread: metadata (ThreadMetadata): Custom metadata attached to a thread. Supports maximum 50 entries. Key length has a limit of 40 characters maximum. Value length has a limit of 4000 characters maximum for strings. resolved (bool): + visibility (ThreadVisibility): updated_at (datetime.datetime): """ @@ -42,6 +45,7 @@ class Thread: created_at: datetime.datetime metadata: ThreadMetadata resolved: bool + visibility: ThreadVisibility updated_at: datetime.datetime additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) @@ -63,6 +67,8 @@ def to_dict(self) -> dict[str, Any]: resolved = self.resolved + visibility = self.visibility.value + updated_at = self.updated_at.isoformat() field_dict: dict[str, Any] = {} @@ -76,6 +82,7 @@ def to_dict(self) -> dict[str, Any]: "createdAt": created_at, "metadata": metadata, "resolved": resolved, + "visibility": visibility, "updatedAt": updated_at, } ) @@ -109,6 +116,8 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: resolved = d.pop("resolved") + visibility = ThreadVisibility(d.pop("visibility")) + updated_at = isoparse(d.pop("updatedAt")) thread = cls( @@ -119,6 +128,7 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: created_at=created_at, metadata=metadata, resolved=resolved, + visibility=visibility, updated_at=updated_at, ) diff --git a/packages/liveblocks-python/liveblocks/models/thread_visibility.py b/packages/liveblocks-python/liveblocks/models/thread_visibility.py new file mode 100644 index 00000000000..e27517542f0 --- /dev/null +++ b/packages/liveblocks-python/liveblocks/models/thread_visibility.py @@ -0,0 +1,6 @@ +from enum import StrEnum + + +class ThreadVisibility(StrEnum): + PRIVATE = "private" + PUBLIC = "public" diff --git a/packages/liveblocks-python/liveblocks/session.py b/packages/liveblocks-python/liveblocks/session.py index 23de587efe8..7e595d77701 100644 --- a/packages/liveblocks-python/liveblocks/session.py +++ b/packages/liveblocks-python/liveblocks/session.py @@ -22,6 +22,12 @@ "comments:read", "comments:write", "comments:none", + "comments:public:read", + "comments:public:write", + "comments:public:none", + "comments:private:read", + "comments:private:write", + "comments:private:none", "feeds:read", "feeds:write", "feeds:none", @@ -40,6 +46,12 @@ "comments:read", "comments:write", "comments:none", + "comments:public:read", + "comments:public:write", + "comments:public:none", + "comments:private:read", + "comments:private:write", + "comments:private:none", "feeds:read", "feeds:write", "feeds:none", diff --git a/packages/liveblocks-python/tests/test_session.py b/packages/liveblocks-python/tests/test_session.py index 071abb1e54f..449ce8fa0ac 100644 --- a/packages/liveblocks-python/tests/test_session.py +++ b/packages/liveblocks-python/tests/test_session.py @@ -8,6 +8,8 @@ P1 = "*:read" P2 = "*:write" P3 = "comments:read" +P4 = "comments:public:write" +P5 = "comments:private:none" def make_session( @@ -83,6 +85,11 @@ def test_permissions_are_additive(self): "bar": [P2], } + def test_accepts_scoped_comments_permissions(self): + assert (make_session().allow("foo", [P1, P4, P5])._serialize_permissions()) == { + "foo": [P1, P4, P5], + } + def test_permissions_are_deduped(self): assert ( make_session() diff --git a/packages/liveblocks-react-ui/src/__tests__/_liveblocks.config.ts b/packages/liveblocks-react-ui/src/__tests__/_liveblocks.config.ts index 0d8ea198608..5256a1a88de 100644 --- a/packages/liveblocks-react-ui/src/__tests__/_liveblocks.config.ts +++ b/packages/liveblocks-react-ui/src/__tests__/_liveblocks.config.ts @@ -1,7 +1,7 @@ import { createClient } from "@liveblocks/client"; import { createRoomContext } from "@liveblocks/react"; -const client = createClient({ +export const client = createClient({ publicApiKey: "pk_localdev", baseUrl: `http://localhost:${process.env.LIVEBLOCKS_DEV_SERVER_PORT ?? 1154}`, polyfills: { WebSocket: globalThis.WebSocket }, diff --git a/packages/liveblocks-react-ui/src/__tests__/index.test.tsx b/packages/liveblocks-react-ui/src/__tests__/index.test.tsx index e317c4caed0..5419782f3b9 100644 --- a/packages/liveblocks-react-ui/src/__tests__/index.test.tsx +++ b/packages/liveblocks-react-ui/src/__tests__/index.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, test } from "vitest"; import { Comment } from "../components/Comment"; import { Composer } from "../components/Composer"; import { Thread } from "../components/Thread"; -import { render } from "./_utils"; // Basically re-exports from @testing-library/react +import { fireEvent, render, screen } from "./_utils"; // Basically re-exports from @testing-library/react const comment: CommentData = { type: "comment", @@ -116,6 +116,7 @@ const thread: ThreadData = { comments: [comment, editedComment, deletedComment], metadata: {}, resolved: false, + visibility: "public", }; describe("Thread", () => { @@ -140,4 +141,31 @@ describe("Composer", () => { expect(container).not.toBeEmptyDOMElement(); }); + + test("should stay expanded after blurring when initially expanded", () => { + render(); + + fireEvent.focus(screen.getByLabelText("Composer editor")); + fireEvent.blur(screen.getByLabelText("Composer editor"), { + relatedTarget: document.body, + }); + + expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument(); + }); + + test("should collapse after blurring when initially collapsed", () => { + render(); + + expect(screen.queryByRole("button", { name: "Send" })).toBeNull(); + + fireEvent.focus(screen.getByLabelText("Composer editor")); + + expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument(); + + fireEvent.blur(screen.getByLabelText("Composer editor"), { + relatedTarget: document.body, + }); + + expect(screen.queryByRole("button", { name: "Send" })).toBeNull(); + }); }); diff --git a/packages/liveblocks-react-ui/src/components/Comment.tsx b/packages/liveblocks-react-ui/src/components/Comment.tsx index 6be10c80e88..54036bee090 100644 --- a/packages/liveblocks-react-ui/src/components/Comment.tsx +++ b/packages/liveblocks-react-ui/src/components/Comment.tsx @@ -10,6 +10,7 @@ import { type GroupMentionData, MENTION_CHARACTER, type MentionData, + type ThreadVisibility, } from "@liveblocks/core"; import { useAddRoomCommentReaction, @@ -66,15 +67,14 @@ import type { } from "../primitives/Comment/types"; import * as ComposerPrimitive from "../primitives/Composer"; import { Timestamp } from "../primitives/Timestamp"; -import { useCurrentUserId } from "../shared"; +import { commentsResourceForVisibility, useCurrentUserId } from "../shared"; import type { CommentAttachmentArgs } from "../types"; import { cn } from "../utils/cn"; import { download } from "../utils/download"; import { useIsGroupMentionMember } from "../utils/use-group-mention"; import { useRefs } from "../utils/use-refs"; import { UserAvatar } from "./Avatar"; -import type { ComposerProps } from "./Composer"; -import { Composer } from "./Composer"; +import { Composer, type ComposerProps } from "./Composer"; import { FileAttachment, MediaAttachment, @@ -100,6 +100,11 @@ export interface CommentProps extends Omit< */ comment: CommentData; + /** + * The visibility of the thread containing the comment. + */ + visibility?: ThreadVisibility; + /** * The comment's avatar. * Can be combined with `Comment.Avatar` to easily follow default styles. @@ -689,6 +694,7 @@ export const Comment = Object.assign( ( { comment, + visibility, indentContent = true, showDeleted, showActions = "hover", @@ -736,7 +742,7 @@ export const Comment = Object.assign( const canComment = useHasPermissionAccess( comment.roomId, - "comments", + commentsResourceForVisibility(visibility), "write" ); @@ -886,6 +892,9 @@ export const Comment = Object.assign( content = ( = { metadata?: never; + /** + * @internal + * The visibility of the parent thread. + */ + visibility?: ThreadVisibility; + /** * The metadata of the comment to create. */ @@ -140,6 +153,12 @@ export type ComposerEditCommentProps = { metadata?: never; + /** + * @internal + * The visibility of the parent thread. + */ + visibility?: ThreadVisibility; + /** * The metadata of the comment to edit. */ @@ -707,6 +726,7 @@ export const Composer = forwardRef( threadId, commentId, metadata, + visibility, commentMetadata, defaultValue, defaultAttachments, @@ -753,8 +773,19 @@ export const Composer = forwardRef( controlledCollapsed, controlledOnCollapsedChange ); - - const canComment = useHasPermissionAccess(roomId, "comments", "write"); + // Only auto-collapse composers that are explicitly meant to support a collapsed state. + const shouldCollapseWhenEmpty = + controlledCollapsed !== undefined || defaultCollapsed === true; + + const commentsResource = + threadId === undefined + ? commentsResourceForVisibility(visibility ?? "public") + : commentsResourceForVisibility(visibility); + const canComment = useHasPermissionAccess( + roomId, + commentsResource, + "write" + ); const setEmptyRef = useCallback((isEmpty: boolean) => { isEmptyRef.current = isEmpty; @@ -791,11 +822,16 @@ export const Composer = forwardRef( event.relatedTarget ?? document.activeElement ); - if (isOutside && isEmptyRef.current && !isEmojiPickerOpenRef.current) { + if ( + shouldCollapseWhenEmpty && + isOutside && + isEmptyRef.current && + !isEmojiPickerOpenRef.current + ) { onCollapsedChange?.(true); } }, - [onBlur, onCollapsedChange] + [onBlur, onCollapsedChange, shouldCollapseWhenEmpty] ); const handleEditorClick = useCallback( @@ -838,6 +874,7 @@ export const Composer = forwardRef( createThread({ body: comment.body, metadata, + visibility, commentMetadata: commentMetadata as CM | undefined, attachments: comment.attachments, }); @@ -850,6 +887,7 @@ export const Composer = forwardRef( editComment, commentMetadata, metadata, + visibility, onComposerSubmit, threadId, ] diff --git a/packages/liveblocks-react-ui/src/components/Thread.tsx b/packages/liveblocks-react-ui/src/components/Thread.tsx index 4b608fbe57c..4f2b2e9c123 100644 --- a/packages/liveblocks-react-ui/src/components/Thread.tsx +++ b/packages/liveblocks-react-ui/src/components/Thread.tsx @@ -46,6 +46,7 @@ import type { ThreadOverrides, } from "../overrides"; import { useOverrides } from "../overrides"; +import { commentsResourceForVisibility } from "../shared"; import { cn } from "../utils/cn"; import { useStableComponent } from "../utils/use-stable-component"; import { useIntersectionCallback } from "../utils/use-visible"; @@ -440,7 +441,7 @@ export const Thread = forwardRef( const canComment = useHasPermissionAccess( thread.roomId, - "comments", + commentsResourceForVisibility(thread.visibility), "write" ); @@ -555,6 +556,7 @@ export const Thread = forwardRef( className="lb-thread-comment" data-unread={isUnread ? "" : undefined} comment={comment} + visibility={thread.visibility} indentContent={indentCommentContent} showDeleted={showDeletedComments} showActions={showActions} @@ -658,6 +660,7 @@ export const Thread = forwardRef( { + test("accepts augmented thread and comment metadata", () => { + void ( + + ); + + void ( + + ); + + void ( + + ); + }); +}); + describe("InboxNotification `kinds` (with Liveblocks augmentation)", () => { test("existing kinds have the expected props types", () => { void ( diff --git a/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx b/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx index 40607d677ff..9b999c52bf0 100644 --- a/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx +++ b/packages/liveblocks-react-ui/test-d/no-augmentation.test-d.tsx @@ -1,10 +1,68 @@ import type { ActivityData, InboxNotificationData } from "@liveblocks/core"; import type { InboxNotificationCustomKindProps } from "@liveblocks/react-ui"; -import { InboxNotification } from "@liveblocks/react-ui"; +import { Composer, InboxNotification } from "@liveblocks/react-ui"; import { describe, expectTypeOf, test } from "vitest"; // TODO: Create type tests for all components/props +describe("Composer (no Liveblocks augmentation)", () => { + test("accepts metadata props for each mode", () => { + void ( + + ); + + void ( + + ); + + void ( + + ); + }); + + test("keeps thread-only props out of reply and edit modes", () => { + void ( + ( + // @ts-expect-error - visibility only applies when creating threads + + ) + ); + void ( + ( + // @ts-expect-error - thread metadata only applies when creating threads + + ) + ); + void ( + ( + // @ts-expect-error - commentId requires threadId + + ) + ); + void ( + ( + // @ts-expect-error - thread metadata only applies when creating threads + + ) + ); + }); +}); + describe("InboxNotification `kinds` (no Liveblocks augmentation)", () => { test("existing kinds have the expected props types", () => { void ( diff --git a/packages/liveblocks-react/src/__tests__/ThreadDB.test.ts b/packages/liveblocks-react/src/__tests__/ThreadDB.test.ts index cffdcc3431a..012e2f49bd8 100644 --- a/packages/liveblocks-react/src/__tests__/ThreadDB.test.ts +++ b/packages/liveblocks-react/src/__tests__/ThreadDB.test.ts @@ -280,6 +280,7 @@ describe("ThreadDB", () => { roomId: "room1", createdAt: new Date("2024-10-09"), resolved: false, + visibility: "private", metadata: { color: "red", tag: "even" }, }); const th3 = dummyThreadData({ @@ -302,6 +303,7 @@ describe("ThreadDB", () => { roomId: "room1", createdAt: new Date("2024-10-12"), resolved: true, + visibility: "private", metadata: { color: "brown", tag: "odd" }, }); @@ -337,6 +339,16 @@ describe("ThreadDB", () => { th5, ]); + // Visibility checks + expect(db.findMany(undefined, { visibility: "private" }, "asc")).toEqual([ + th2, + th5, + ]); + expect(db.findMany(undefined, { visibility: "public" }, "asc")).toEqual([ + th1, + th3, + ]); + // Metadata checks { const query = { metadata: { color: "red" } }; diff --git a/packages/liveblocks-react/src/__tests__/_dummies.ts b/packages/liveblocks-react/src/__tests__/_dummies.ts index 4fbbea185cd..89e11ebfb39 100644 --- a/packages/liveblocks-react/src/__tests__/_dummies.ts +++ b/packages/liveblocks-react/src/__tests__/_dummies.ts @@ -30,6 +30,7 @@ export function dummyThreadData({ roomId, metadata: {}, resolved: false, + visibility: "public", ...overrides, comments: overrides.comments ? overrides.comments.map((comment) => ({ ...comment, threadId })) diff --git a/packages/liveblocks-react/src/__tests__/_restMocks.ts b/packages/liveblocks-react/src/__tests__/_restMocks.ts index 0acfce78885..f3fa6257246 100644 --- a/packages/liveblocks-react/src/__tests__/_restMocks.ts +++ b/packages/liveblocks-react/src/__tests__/_restMocks.ts @@ -11,6 +11,7 @@ import type { SubscriptionData, ThreadData, ThreadDataWithDeleteInfo, + ThreadVisibility, } from "@liveblocks/core"; import type { HttpResponseResolver } from "msw"; import { http } from "msw"; @@ -63,6 +64,7 @@ export function mockCreateThread< { roomId: string }, { id: string; + visibility?: ThreadVisibility; metadata?: TM; comment: { id: string; body: CommentBody; metadata?: CM }; }, diff --git a/packages/liveblocks-react/src/__tests__/_utils.tsx b/packages/liveblocks-react/src/__tests__/_utils.tsx index 444c0ff3561..6338d8ea629 100644 --- a/packages/liveblocks-react/src/__tests__/_utils.tsx +++ b/packages/liveblocks-react/src/__tests__/_utils.tsx @@ -131,6 +131,7 @@ const parser = new QueryParser({ fields: { resolved: "boolean", subscribed: "boolean", + visibility: "string", }, indexableFields: { metadata: "mixed", diff --git a/packages/liveblocks-react/src/__tests__/index.test.tsx b/packages/liveblocks-react/src/__tests__/index.test.tsx index d1e642ed5fc..da5666c2116 100644 --- a/packages/liveblocks-react/src/__tests__/index.test.tsx +++ b/packages/liveblocks-react/src/__tests__/index.test.tsx @@ -3,6 +3,7 @@ import { ClientMsgCode, ServerMsgCode, wait } from "@liveblocks/core"; import { render } from "@testing-library/react"; import { http, HttpResponse } from "msw"; import { setupServer } from "msw/node"; +import type { ReactNode } from "react"; import { afterAll, afterEach, @@ -14,8 +15,10 @@ import { vi, } from "vitest"; +import { useHasPermissionAccess } from "../_private"; import { createRoomContext, useRoom as useRoomGlobal } from "../room"; import { + RoomProvider, useCanRedo, useCanUndo, useHistory, @@ -306,6 +309,68 @@ describe("useOthers", () => { }); }); +describe("useHasPermissionAccess", () => { + test("optimistically allows writing to scoped comments resources before permission hints arrive", () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + const publicAccess = renderHook( + () => useHasPermissionAccess("room", "comments:public", "write"), + { wrapper } + ); + const privateAccess = renderHook( + () => useHasPermissionAccess("room", "comments:private", "write"), + { wrapper } + ); + + expect(publicAccess.result.current).toBe(true); + expect(privateAccess.result.current).toBe(true); + }); + + test("uses aggregate and scoped comment access from the connection before permission hints arrive", async () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + const access = renderHook( + () => ({ + comments: useHasPermissionAccess("room", "comments", "write"), + public: useHasPermissionAccess("room", "comments:public", "write"), + private: useHasPermissionAccess("room", "comments:private", "write"), + }), + { wrapper } + ); + + const sim = await websocketSimulator(); + act(() => + sim.simulateIncomingMessage({ + type: ServerMsgCode.ROOM_STATE, + actor: 0, + nonce: "nonce-for-actor-0", + scopes: [ + "room:write", + "comments:none", + "comments:public:write", + "comments:private:none", + ], + users: {}, + meta: {}, + }) + ); + + expect(access.result.current).toEqual({ + comments: true, + public: true, + private: false, + }); + }); +}); + describe("useStorage", () => { test("return null before storage has loaded", () => { const { result } = renderHook(() => useStorage((root) => root.obj)); diff --git a/packages/liveblocks-react/src/__tests__/umbrella-store/_dummies.ts b/packages/liveblocks-react/src/__tests__/umbrella-store/_dummies.ts index 92d4c91c81f..ac974ed4107 100644 --- a/packages/liveblocks-react/src/__tests__/umbrella-store/_dummies.ts +++ b/packages/liveblocks-react/src/__tests__/umbrella-store/_dummies.ts @@ -16,6 +16,7 @@ export function createThread( comments = [], metadata = {}, resolved = false, + visibility = "public", } = overrides; const createdAt = overrides.createdAt ?? new Date(); @@ -30,6 +31,7 @@ export function createThread( comments, metadata, resolved, + visibility, }; } diff --git a/packages/liveblocks-react/src/__tests__/useCreateThread.test.tsx b/packages/liveblocks-react/src/__tests__/useCreateThread.test.tsx index e795817203f..98a05614205 100644 --- a/packages/liveblocks-react/src/__tests__/useCreateThread.test.tsx +++ b/packages/liveblocks-react/src/__tests__/useCreateThread.test.tsx @@ -38,6 +38,7 @@ describe("useCreateThread", () => { test("should create a thread optimistically and override with thread coming from server", async () => { const roomId = nanoid(); const fakeCreatedAt = addMinutes(new Date(), 5); + const visibility = "private"; server.use( mockGetThreads(() => { @@ -56,6 +57,7 @@ describe("useCreateThread", () => { }), mockCreateThread(async ({ request }) => { const json = await request.json(); + expect(json.visibility).toBe(visibility); const comment = dummyCommentData({ roomId, @@ -70,6 +72,7 @@ describe("useCreateThread", () => { id: json.id, comments: [comment], createdAt: fakeCreatedAt, + visibility: json.visibility, }); return HttpResponse.json(thread); @@ -112,10 +115,12 @@ describe("useCreateThread", () => { version: 1, content: [{ type: "paragraph", children: [{ text: "Hello" }] }], }, + visibility, }); }); expect(result.current.threadData.threads?.[0]).toEqual(thread); + expect(thread.visibility).toBe(visibility); // We're using the createdDate overriden by the server to ensure the optimistic update have been properly deleted await vi.waitFor(() => diff --git a/packages/liveblocks-react/src/__tests__/useThreads.test.tsx b/packages/liveblocks-react/src/__tests__/useThreads.test.tsx index d2be4393d62..05340ab8db0 100644 --- a/packages/liveblocks-react/src/__tests__/useThreads.test.tsx +++ b/packages/liveblocks-react/src/__tests__/useThreads.test.tsx @@ -257,16 +257,17 @@ describe("useThreads", () => { test("should fetch threads for a given query", async () => { const roomId = nanoid(); - const pinnedThread = dummyThreadData({ + const privatePinnedThread = dummyThreadData({ roomId, + visibility: "private", metadata: { pinned: true, }, }); - const unpinnedThread = dummyThreadData({ + const publicPinnedThread = dummyThreadData({ roomId, metadata: { - pinned: false, + pinned: true, }, }); @@ -275,7 +276,10 @@ describe("useThreads", () => { const url = new URL(request.url); const query = url.searchParams.get("query"); const pred = query ? makeThreadFilter(query) : () => true; - const filteredThreads = [pinnedThread, unpinnedThread].filter(pred); + const filteredThreads = [ + privatePinnedThread, + publicPinnedThread, + ].filter(pred); const subscriptions = filteredThreads.map((thread) => dummySubscriptionData({ subjectId: thread.id }) ); @@ -302,7 +306,10 @@ describe("useThreads", () => { }>(); const { result, unmount } = renderHook( - () => useThreads({ query: { metadata: { pinned: true } } }), + () => + useThreads({ + query: { visibility: "private", metadata: { pinned: true } }, + }), { wrapper: ({ children }) => ( {children} @@ -315,7 +322,7 @@ describe("useThreads", () => { await vi.waitFor(() => expect(result.current).toEqual({ isLoading: false, - threads: [pinnedThread], + threads: [privatePinnedThread], fetchMore: expect.any(Function), isFetchingMore: false, hasFetchedAll: true, diff --git a/packages/liveblocks-react/src/__tests__/useUserThreads.test.tsx b/packages/liveblocks-react/src/__tests__/useUserThreads.test.tsx index 3f708c89728..1f90fdad564 100644 --- a/packages/liveblocks-react/src/__tests__/useUserThreads.test.tsx +++ b/packages/liveblocks-react/src/__tests__/useUserThreads.test.tsx @@ -150,21 +150,22 @@ describe("useUserThreads", () => { test("should fetch user threads for given query on mount", async () => { const roomId = nanoid(); - const pinnedThread = dummyThreadData({ + const privatePinnedThread = dummyThreadData({ roomId, + visibility: "private", metadata: { pinned: true, }, }); - const unpinnedThread = dummyThreadData({ + const publicPinnedThread = dummyThreadData({ roomId, metadata: { - pinned: false, + pinned: true, }, }); const subscriptions = [ - dummySubscriptionData({ subjectId: pinnedThread.id }), - dummySubscriptionData({ subjectId: unpinnedThread.id }), + dummySubscriptionData({ subjectId: privatePinnedThread.id }), + dummySubscriptionData({ subjectId: publicPinnedThread.id }), ]; server.use( @@ -173,7 +174,7 @@ describe("useUserThreads", () => { const query = url.searchParams.get("query"); const pred = query ? makeThreadFilter(query) : () => true; return HttpResponse.json({ - threads: [pinnedThread, unpinnedThread].filter(pred), + threads: [privatePinnedThread, publicPinnedThread].filter(pred), inboxNotifications: [], subscriptions, meta: { @@ -193,7 +194,9 @@ describe("useUserThreads", () => { const { result, unmount } = renderHook( () => - useUserThreads_experimental({ query: { metadata: { pinned: true } } }), + useUserThreads_experimental({ + query: { visibility: "private", metadata: { pinned: true } }, + }), { wrapper: ({ children }) => ( {children} @@ -208,7 +211,7 @@ describe("useUserThreads", () => { await vi.waitFor(() => expect(result.current).toEqual({ isLoading: false, - threads: [pinnedThread], + threads: [privatePinnedThread], fetchMore: expect.any(Function), isFetchingMore: false, hasFetchedAll: true, diff --git a/packages/liveblocks-react/src/lib/querying.ts b/packages/liveblocks-react/src/lib/querying.ts index 00d8d471267..208e41df90e 100644 --- a/packages/liveblocks-react/src/lib/querying.ts +++ b/packages/liveblocks-react/src/lib/querying.ts @@ -44,6 +44,7 @@ function matchesThreadsQuery( return ( (q.resolved === undefined || thread.resolved === q.resolved) && + (q.visibility === undefined || thread.visibility === q.visibility) && (q.subscribed === undefined || (q.subscribed === true && subscription !== undefined) || (q.subscribed === false && subscription === undefined)) diff --git a/packages/liveblocks-react/src/room.tsx b/packages/liveblocks-react/src/room.tsx index 7d4d06985c5..75fc2ff7504 100644 --- a/packages/liveblocks-react/src/room.tsx +++ b/packages/liveblocks-react/src/room.tsx @@ -225,6 +225,13 @@ function getRoomExtrasForClient< }; } +function getThreadVisibility( + store: UmbrellaStore, + threadId: string +): ThreadData["visibility"] | undefined { + return store.outputs.threads.get().getEvenIfDeleted(threadId)?.visibility; +} + function makeRoomExtrasForClient(client: OpaqueClient) { const store = getUmbrellaStoreForClient(client); @@ -1934,6 +1941,7 @@ function useCreateRoomThread( const metadata = options.metadata ?? ({} as TM); const commentMetadata = options.commentMetadata ?? ({} as CM); const attachments = options.attachments; + const visibility = options.visibility ?? "public"; const threadId = createThreadId(); const commentId = createCommentId(); @@ -1960,6 +1968,7 @@ function useCreateRoomThread( metadata, comments: [newComment], resolved: false, + visibility, }; const { store, onMutationFailure } = getRoomExtrasForClient(client); @@ -1977,6 +1986,7 @@ function useCreateRoomThread( threadId, commentId, body, + visibility, metadata, commentMetadata, attachmentIds, @@ -1995,6 +2005,7 @@ function useCreateRoomThread( threadId, commentId, body, + visibility, metadata, commentMetadata, }, @@ -2041,18 +2052,24 @@ function useDeleteRoomThread(roomId: string): (threadId: string) => void { deletedAt: new Date(), }); - client[kInternal].httpClient.deleteThread({ roomId, threadId }).then( - () => { - // Replace the optimistic update by the real thing - store.deleteThread(threadId, optimisticId); - }, - (err: Error) => - onMutationFailure( - optimisticId, - { type: "DELETE_THREAD_ERROR", roomId, threadId }, - err - ) - ); + client[kInternal].httpClient + .deleteThread({ + roomId, + threadId, + visibility: existing.visibility, + }) + .then( + () => { + // Replace the optimistic update by the real thing + store.deleteThread(threadId, optimisticId); + }, + (err: Error) => + onMutationFailure( + optimisticId, + { type: "DELETE_THREAD_ERROR", roomId, threadId }, + err + ) + ); }, [client, roomId] ); @@ -2092,7 +2109,12 @@ function useEditRoomThreadMetadata(roomId: string) { }); client[kInternal].httpClient - .editThreadMetadata({ roomId, threadId, metadata }) + .editThreadMetadata({ + roomId, + threadId, + metadata, + visibility: getThreadVisibility(store, threadId), + }) .then( (metadata) => // Replace the optimistic update by the real thing @@ -2152,7 +2174,13 @@ function useEditRoomCommentMetadata(roomId: string) { }); client[kInternal].httpClient - .editCommentMetadata({ roomId, threadId, commentId, metadata }) + .editCommentMetadata({ + roomId, + threadId, + commentId, + metadata, + visibility: getThreadVisibility(store, threadId), + }) .then( (updatedMetadata) => // Replace the optimistic update by the real thing @@ -2247,6 +2275,7 @@ function useCreateRoomComment( body, metadata, attachmentIds, + visibility: getThreadVisibility(store, threadId), }) .then( (newComment) => { @@ -2363,6 +2392,7 @@ function useEditRoomComment( body, attachmentIds, metadata, + visibility: existing.visibility, }) .then( (editedComment) => { @@ -2430,7 +2460,12 @@ function useDeleteRoomComment(roomId: string) { }); client[kInternal].httpClient - .deleteComment({ roomId, threadId, commentId }) + .deleteComment({ + roomId, + threadId, + commentId, + visibility: getThreadVisibility(store, threadId), + }) .then( () => { // Replace the optimistic update by the real thing @@ -2485,7 +2520,13 @@ function useAddRoomCommentReaction(roomId: string) { }); client[kInternal].httpClient - .addReaction({ roomId, threadId, commentId, emoji }) + .addReaction({ + roomId, + threadId, + commentId, + emoji, + visibility: getThreadVisibility(store, threadId), + }) .then( (addedReaction) => { // Replace the optimistic update by the real thing @@ -2557,7 +2598,13 @@ function useRemoveRoomCommentReaction(roomId: string) { }); client[kInternal].httpClient - .removeReaction({ roomId, threadId, commentId, emoji }) + .removeReaction({ + roomId, + threadId, + commentId, + emoji, + visibility: getThreadVisibility(store, threadId), + }) .then( () => { // Replace the optimistic update by the real thing @@ -2704,7 +2751,11 @@ function useMarkRoomThreadAsResolved(roomId: string) { }); client[kInternal].httpClient - .markThreadAsResolved({ roomId, threadId }) + .markThreadAsResolved({ + roomId, + threadId, + visibility: getThreadVisibility(store, threadId), + }) .then( () => { // Replace the optimistic update by the real thing @@ -2764,7 +2815,11 @@ function useMarkRoomThreadAsUnresolved(roomId: string) { }); client[kInternal].httpClient - .markThreadAsUnresolved({ roomId, threadId }) + .markThreadAsUnresolved({ + roomId, + threadId, + visibility: getThreadVisibility(store, threadId), + }) .then( () => { // Replace the optimistic update by the real thing @@ -3754,12 +3809,23 @@ function useSelfAccessFallback( ); const getSnapshot = useCallback(() => { - const self = room?.id === roomId ? room.getSelf() : null; - if (resource === "comments" && requiredAccess === "write") { - return self?.canComment ?? true; + const isCommentsResource = + resource === "comments" || + resource === "comments:public" || + resource === "comments:private"; + + if (room?.id === roomId) { + const permissionMatrix = room[kInternal].getPermissionMatrix(); + if (permissionMatrix !== undefined) { + return hasPermissionAccess(permissionMatrix, resource, requiredAccess); + } } - if (resource === "storage" && requiredAccess === "write") { - return self?.canWrite ?? true; + + if ( + requiredAccess === "write" && + (isCommentsResource || resource === "storage") + ) { + return true; } return false; diff --git a/packages/liveblocks-react/src/types/index.ts b/packages/liveblocks-react/src/types/index.ts index 7fd0f6eddf7..46ba5020ea8 100644 --- a/packages/liveblocks-react/src/types/index.ts +++ b/packages/liveblocks-react/src/types/index.ts @@ -50,6 +50,7 @@ import type { SearchCommentsResult, SyncStatus, ThreadData, + ThreadVisibility, ToJson, UrlMetadata, WithNavigation, @@ -130,6 +131,11 @@ export type ThreadsQuery = { */ resolved?: boolean; + /** + * Whether to only return public or private threads. If not provided, all threads will be returned. + */ + visibility?: ThreadVisibility; + /** * Whether to only return threads that the user is subscribed to or not. If not provided, * all threads will be returned. @@ -269,7 +275,7 @@ export type GroupAsyncSuccess = AsyncSuccess; // prettier-ignore export type CreateThreadOptions = Resolve< - { body: CommentBody, attachments?: CommentAttachment[]; } + { body: CommentBody, attachments?: CommentAttachment[]; visibility?: ThreadVisibility; } & PartialUnless & PartialUnless >; diff --git a/packages/liveblocks-react/test-d/augmentation.test-d.tsx b/packages/liveblocks-react/test-d/augmentation.test-d.tsx index f94bbecedcf..3734644b283 100644 --- a/packages/liveblocks-react/test-d/augmentation.test-d.tsx +++ b/packages/liveblocks-react/test-d/augmentation.test-d.tsx @@ -802,6 +802,7 @@ describe("with Liveblocks augmentation", () => { }, metadata: { color: "red" }, commentMetadata: { priority: 1 }, + visibility: "private", }); expectTypeOf(thread.type).toEqualTypeOf<"thread">(); diff --git a/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx b/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx index da9e28ef1cb..a176af4494f 100644 --- a/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx +++ b/packages/liveblocks-react/test-d/no-augmentation.test-d.tsx @@ -641,6 +641,7 @@ describe("without Liveblocks augmentation", () => { version: 1, content: [{ type: "paragraph", children: [{ text: "hi" }] }], }, + visibility: "private", }); expectTypeOf(thread1.type).toEqualTypeOf<"thread">(); From 9e025345551dc80c26a0443fbd605112d6e8365b Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 29 Jun 2026 12:01:37 +0200 Subject: [PATCH 2/8] Update version history endpoints prior to adding support for Storage (PR 1791) Original commit: 21e16a8bb8b6d398ee3f206c727a0f03c121b756 --- packages/liveblocks-server/src/index.ts | 1 + packages/liveblocks-server/src/lib/semver.ts | 42 ++++++++++++ .../liveblocks-server/test/semver.test.ts | 64 +++++++++++++++++++ tools/liveblocks-cli/package.json | 2 +- tools/liveblocks-cli/src/dev-server/auth.ts | 6 +- .../src/dev-server/routes/client-api.ts | 8 ++- .../src/dev-server/routes/rest-api.ts | 4 +- tools/liveblocks-cli/src/upgrade/index.ts | 16 +---- tools/liveblocks-cli/test/upgrade.test.ts | 34 +--------- 9 files changed, 122 insertions(+), 55 deletions(-) create mode 100644 packages/liveblocks-server/src/lib/semver.ts create mode 100644 packages/liveblocks-server/test/semver.test.ts diff --git a/packages/liveblocks-server/src/index.ts b/packages/liveblocks-server/src/index.ts index 95d74512d81..7c72663b7f6 100644 --- a/packages/liveblocks-server/src/index.ts +++ b/packages/liveblocks-server/src/index.ts @@ -53,6 +53,7 @@ export * from "~/lib/tryCatch"; // YYY Maybe isolate these simple/common datastructures into a separate NPM package? export * from "~/lib/DefaultMap"; export * from "~/lib/NestedMap"; +export * from "~/lib/semver"; export { quote } from "~/lib/text"; export * from "~/lib/UniqueMap"; diff --git a/packages/liveblocks-server/src/lib/semver.ts b/packages/liveblocks-server/src/lib/semver.ts new file mode 100644 index 00000000000..0687b4bb34b --- /dev/null +++ b/packages/liveblocks-server/src/lib/semver.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Liveblocks Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * Compares two semver-ish version strings (`major.minor.patch` with an optional + * `-prerelease` suffix). Returns a negative number if `a < b`, a positive number + * if `a > b`, and `0` if they're equal. + * + * Missing numeric components are treated as `0`, so `"3.21"` and `"3.21.0"` + * compare equal. A final release outranks any of its prereleases + * (`"3.14.0" > "3.14.0-rc1"`); prereleases are otherwise compared lexically. + */ +export function cmpSemver(a: string, b: string): number { + const [coreA = "", preA] = a.split("-", 2); + const [coreB = "", preB] = b.split("-", 2); + const partsA = coreA.split(".").map(Number); + const partsB = coreB.split(".").map(Number); + for (let i = 0; i < 3; i++) { + const pa = partsA[i] ?? 0; + const pb = partsB[i] ?? 0; + if (pa !== pb) return pa - pb; + } + // A final release outranks any of its prereleases (3.14.0 > 3.14.0-rc1) + if (!preA && preB) return 1; + if (preA && !preB) return -1; + if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0; + return 0; +} diff --git a/packages/liveblocks-server/test/semver.test.ts b/packages/liveblocks-server/test/semver.test.ts new file mode 100644 index 00000000000..a29da4d5b7c --- /dev/null +++ b/packages/liveblocks-server/test/semver.test.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) Liveblocks Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { describe, expect, test } from "vitest"; + +import { cmpSemver } from "../src/lib/semver"; + +describe("cmpSemver", () => { + test("compares major versions", () => { + expect(cmpSemver("2.0.0", "3.0.0")).toBeLessThan(0); + expect(cmpSemver("3.0.0", "2.0.0")).toBeGreaterThan(0); + }); + + test("compares minor versions", () => { + expect(cmpSemver("3.13.0", "3.14.0")).toBeLessThan(0); + expect(cmpSemver("3.14.0", "3.13.0")).toBeGreaterThan(0); + }); + + test("compares patch versions", () => { + expect(cmpSemver("3.14.0", "3.14.1")).toBeLessThan(0); + expect(cmpSemver("3.14.1", "3.14.0")).toBeGreaterThan(0); + }); + + test("treats equal versions as equal", () => { + expect(cmpSemver("3.14.0", "3.14.0")).toBe(0); + }); + + test("a final release outranks any of its prereleases", () => { + expect(cmpSemver("3.14.0", "3.14.0-rc1")).toBeGreaterThan(0); + expect(cmpSemver("3.14.0-rc1", "3.14.0")).toBeLessThan(0); + }); + + test("compares prereleases lexically", () => { + expect(cmpSemver("3.14.0-rc1", "3.14.0-rc2")).toBeLessThan(0); + expect(cmpSemver("3.14.0-rc2", "3.14.0-rc1")).toBeGreaterThan(0); + expect(cmpSemver("3.14.0-rc1", "3.14.0-rc1")).toBe(0); + }); + + test("treats a missing patch component as 0", () => { + // "3.21" and "3.21.0" are equivalent thresholds + expect(cmpSemver("3.21", "3.21.0")).toBe(0); + expect(cmpSemver("3.21", "3.20.0")).toBeGreaterThan(0); + expect(cmpSemver("3.21", "3.22.0")).toBeLessThan(0); + }); + + test("the comparison is sign-consistent when operands swap", () => { + expect(cmpSemver("3.19.5", "3.21.0")).toBeLessThan(0); + expect(cmpSemver("3.21.0", "3.19.5")).toBeGreaterThan(0); + }); +}); diff --git a/tools/liveblocks-cli/package.json b/tools/liveblocks-cli/package.json index 1bfe2706a92..8d6836419e1 100644 --- a/tools/liveblocks-cli/package.json +++ b/tools/liveblocks-cli/package.json @@ -49,7 +49,7 @@ "@liveblocks/core": "v3.21.0-private3", "@liveblocks/query-parser": "workspace:^", "@liveblocks/server": "workspace:^", - "@liveblocks/zenrouter": "^1.0.18", + "@liveblocks/zenrouter": "^1.1.0", "decoders": "^2.9.0", "js-base64": "^3.7.5", "yjs": "^13.6.10" diff --git a/tools/liveblocks-cli/src/dev-server/auth.ts b/tools/liveblocks-cli/src/dev-server/auth.ts index 1e122b174cb..4ee25299c59 100644 --- a/tools/liveblocks-cli/src/dev-server/auth.ts +++ b/tools/liveblocks-cli/src/dev-server/auth.ts @@ -15,7 +15,11 @@ * along with this program. If not, see . */ -import { mergeRoomPermissionScopes, nanoid, Permission } from "@liveblocks/core"; +import { + mergeRoomPermissionScopes, + nanoid, + Permission, +} from "@liveblocks/core"; import type { CreateTicketOptions } from "@liveblocks/server"; import { ProtocolVersion } from "@liveblocks/server"; diff --git a/tools/liveblocks-cli/src/dev-server/routes/client-api.ts b/tools/liveblocks-cli/src/dev-server/routes/client-api.ts index 5ea6c9aa577..e6c2799239f 100644 --- a/tools/liveblocks-cli/src/dev-server/routes/client-api.ts +++ b/tools/liveblocks-cli/src/dev-server/routes/client-api.ts @@ -151,8 +151,12 @@ zen.route("POST /v2/c/rooms//text-metadata", () => { zen.route("POST /v2/c/rooms//attachments/presigned-urls", () => NOT_IMPLEMENTED()); zen.route("POST /v2/c/rooms//send-message", () => NOT_IMPLEMENTED()); zen.route("GET /v2/c/rooms//storage", () => NOT_IMPLEMENTED()); + zen.route("GET /v2/c/rooms//versions", () => NOT_IMPLEMENTED()); + zen.route("GET /v2/c/rooms//versions/delta", () => NOT_IMPLEMENTED()); + // zen.route("GET /v2/c/rooms//versions//storage", () => NOT_IMPLEMENTED()); + zen.route("GET /v2/c/rooms//versions//yjs", () => NOT_IMPLEMENTED()); + zen.route("GET /v2/c/rooms//y-version/", () => NOT_IMPLEMENTED()); // Deprecated, replaced by GET /v2/c/rooms//versions//yjs zen.route("POST /v2/c/rooms//version", () => NOT_IMPLEMENTED()); - zen.route("GET /v2/c/rooms//y-version/", () => NOT_IMPLEMENTED()); zen.route("POST /v2/c/rooms//ai/contextual-prompt", () => NOT_IMPLEMENTED()); zen.route("POST /v2/c/rooms//threads", () => NOT_IMPLEMENTED()); zen.route("POST /v2/c/rooms//threads/search", () => NOT_IMPLEMENTED()); @@ -185,7 +189,5 @@ zen.route("POST /v2/c/rooms//text-metadata", () => { zen.route("POST /v2/c/notification-settings", () => NOT_IMPLEMENTED()); zen.route("GET /v2/c/rooms//thread-with-notification/", () => NOT_IMPLEMENTED()); zen.route("GET /v2/c/urls/metadata", () => NOT_IMPLEMENTED()); - zen.route("GET /v2/c/rooms//versions", () => NOT_IMPLEMENTED()); - zen.route("GET /v2/c/rooms//versions/delta", () => NOT_IMPLEMENTED()); zen.route("POST /v2/c/groups/find", () => NOT_IMPLEMENTED()); } diff --git a/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts b/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts index d1746cd4f9d..f42db27a560 100644 --- a/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts +++ b/tools/liveblocks-cli/src/dev-server/routes/rest-api.ts @@ -687,8 +687,8 @@ zen.route( zen.route("GET /v2/rooms//prewarm", () => NOT_IMPLEMENTED()); zen.route("POST /v2/rooms//broadcast_event", () => NOT_IMPLEMENTED()); zen.route("GET /v2/rooms//versions", () => NOT_IMPLEMENTED()); - zen.route("GET /v2/rooms//version/", () => NOT_IMPLEMENTED()); - zen.route("POST /v2/rooms//version", () => NOT_IMPLEMENTED()); + zen.route("GET /v2/rooms//versions//yjs", () => NOT_IMPLEMENTED()); + zen.route("POST /v2/rooms//versions", () => NOT_IMPLEMENTED()); zen.route("POST /v2/rooms//threads", () => NOT_IMPLEMENTED()); zen.route("GET /v2/rooms//threads/", () => NOT_IMPLEMENTED()); zen.route("POST /v2/rooms//threads//mark-as-resolved", () => NOT_IMPLEMENTED()); diff --git a/tools/liveblocks-cli/src/upgrade/index.ts b/tools/liveblocks-cli/src/upgrade/index.ts index be45a4dd360..1c5f28e849e 100644 --- a/tools/liveblocks-cli/src/upgrade/index.ts +++ b/tools/liveblocks-cli/src/upgrade/index.ts @@ -15,6 +15,7 @@ * along with this program. If not, see . */ +import { cmpSemver } from "@liveblocks/server"; import { execFileSync } from "child_process"; import { existsSync, readFileSync } from "fs"; import { resolve } from "path"; @@ -36,21 +37,6 @@ function findLiveblocksDependencies( ); } -export function cmpSemver(a: string, b: string): number { - const [coreA, preA] = a.split("-", 2); - const [coreB, preB] = b.split("-", 2); - const partsA = coreA.split(".").map(Number); - const partsB = coreB.split(".").map(Number); - for (let i = 0; i < 3; i++) { - if (partsA[i] !== partsB[i]) return partsA[i] - partsB[i]; - } - // No prerelease > any prerelease (3.14.0 > 3.14.0-rc1) - if (!preA && preB) return 1; - if (preA && !preB) return -1; - if (preA && preB) return preA < preB ? -1 : preA > preB ? 1 : 0; - return 0; -} - /** * Given the parsed JSON output of `npm view ... version --json`, returns the * highest version. npm returns a single string when one version matches, or diff --git a/tools/liveblocks-cli/test/upgrade.test.ts b/tools/liveblocks-cli/test/upgrade.test.ts index 72f03c6256e..829f2560a0c 100644 --- a/tools/liveblocks-cli/test/upgrade.test.ts +++ b/tools/liveblocks-cli/test/upgrade.test.ts @@ -17,39 +17,7 @@ import { describe, expect, test } from "bun:test"; -import { cmpSemver, highestVersion } from "~/upgrade/index"; - -describe("compareSemver", () => { - test("compares major versions", () => { - expect(cmpSemver("2.0.0", "3.0.0")).toBeLessThan(0); - expect(cmpSemver("3.0.0", "2.0.0")).toBeGreaterThan(0); - }); - - test("compares minor versions", () => { - expect(cmpSemver("3.13.0", "3.14.0")).toBeLessThan(0); - expect(cmpSemver("3.14.0", "3.13.0")).toBeGreaterThan(0); - }); - - test("compares patch versions", () => { - expect(cmpSemver("3.14.0", "3.14.1")).toBeLessThan(0); - expect(cmpSemver("3.14.1", "3.14.0")).toBeGreaterThan(0); - }); - - test("equal versions", () => { - expect(cmpSemver("3.14.0", "3.14.0")).toBe(0); - }); - - test("release > prerelease", () => { - expect(cmpSemver("3.14.0", "3.14.0-rc1")).toBeGreaterThan(0); - expect(cmpSemver("3.14.0-rc1", "3.14.0")).toBeLessThan(0); - }); - - test("compares prerelease strings", () => { - expect(cmpSemver("3.14.0-rc1", "3.14.0-rc2")).toBeLessThan(0); - expect(cmpSemver("3.14.0-rc2", "3.14.0-rc1")).toBeGreaterThan(0); - expect(cmpSemver("3.14.0-rc1", "3.14.0-rc1")).toBe(0); - }); -}); +import { highestVersion } from "~/upgrade/index"; describe("pickHighestVersion", () => { // Simulates: npm view @liveblocks/core@latest version --json From b4ef2b7d6bfcde0ecab71bac65f36cb3c3a4de5c Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 29 Jun 2026 12:09:48 +0200 Subject: [PATCH 3/8] Add private comments example (#3548) Co-authored-by: Chris Nicholas --- examples/nextjs-comments-private/.env.example | 2 + examples/nextjs-comments-private/.gitignore | 12 + examples/nextjs-comments-private/.prettierrc | 11 + examples/nextjs-comments-private/README.md | 87 + .../liveblocks.config.ts | 16 + .../nextjs-comments-private/next.config.js | 7 + .../nextjs-comments-private/package-lock.json | 3293 +++++++++++++++++ examples/nextjs-comments-private/package.json | 29 + .../src/app/Providers.tsx | 92 + .../src/app/api/liveblocks-auth/route.ts | 47 + .../src/app/api/users/route.ts | 20 + .../src/app/api/users/search/route.ts | 20 + .../src/app/layout.tsx | 33 + .../nextjs-comments-private/src/app/page.tsx | 178 + .../src/components/Loading.tsx | 7 + .../nextjs-comments-private/src/database.ts | 93 + .../src/styles/globals.css | 264 ++ examples/nextjs-comments-private/src/user.ts | 19 + .../nextjs-comments-private/tsconfig.json | 43 + examples/nextjs-comments-private/vercel.json | 4 + 20 files changed, 4277 insertions(+) create mode 100644 examples/nextjs-comments-private/.env.example create mode 100644 examples/nextjs-comments-private/.gitignore create mode 100644 examples/nextjs-comments-private/.prettierrc create mode 100644 examples/nextjs-comments-private/README.md create mode 100644 examples/nextjs-comments-private/liveblocks.config.ts create mode 100644 examples/nextjs-comments-private/next.config.js create mode 100644 examples/nextjs-comments-private/package-lock.json create mode 100644 examples/nextjs-comments-private/package.json create mode 100644 examples/nextjs-comments-private/src/app/Providers.tsx create mode 100644 examples/nextjs-comments-private/src/app/api/liveblocks-auth/route.ts create mode 100644 examples/nextjs-comments-private/src/app/api/users/route.ts create mode 100644 examples/nextjs-comments-private/src/app/api/users/search/route.ts create mode 100644 examples/nextjs-comments-private/src/app/layout.tsx create mode 100644 examples/nextjs-comments-private/src/app/page.tsx create mode 100644 examples/nextjs-comments-private/src/components/Loading.tsx create mode 100644 examples/nextjs-comments-private/src/database.ts create mode 100644 examples/nextjs-comments-private/src/styles/globals.css create mode 100644 examples/nextjs-comments-private/src/user.ts create mode 100644 examples/nextjs-comments-private/tsconfig.json create mode 100644 examples/nextjs-comments-private/vercel.json diff --git a/examples/nextjs-comments-private/.env.example b/examples/nextjs-comments-private/.env.example new file mode 100644 index 00000000000..9c176850c85 --- /dev/null +++ b/examples/nextjs-comments-private/.env.example @@ -0,0 +1,2 @@ +# https://liveblocks.io/dashboard/apikeys +LIVEBLOCKS_SECRET_KEY= diff --git a/examples/nextjs-comments-private/.gitignore b/examples/nextjs-comments-private/.gitignore new file mode 100644 index 00000000000..3a68e0cfc9d --- /dev/null +++ b/examples/nextjs-comments-private/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +.env +.env.* +!.env.example +*.tsbuildinfo +.vercel +.next +out +next-env.d.ts +# Turborepo +.turbo diff --git a/examples/nextjs-comments-private/.prettierrc b/examples/nextjs-comments-private/.prettierrc new file mode 100644 index 00000000000..06998724304 --- /dev/null +++ b/examples/nextjs-comments-private/.prettierrc @@ -0,0 +1,11 @@ +{ + "semi": true, + "tabWidth": 2, + "useTabs": false, + "singleQuote": false, + "jsxSingleQuote": false, + "arrowParens": "always", + "bracketSpacing": true, + "bracketSameLine": false, + "trailingComma": "es5" +} diff --git a/examples/nextjs-comments-private/README.md b/examples/nextjs-comments-private/README.md new file mode 100644 index 00000000000..a400f3b09bc --- /dev/null +++ b/examples/nextjs-comments-private/README.md @@ -0,0 +1,87 @@ +

+ + Liveblocks + + + Liveblocks + +

+ +# Private Commenting + +

+ + Live Preview + + + Open in CodeSandbox + + React + Next.js +

+ +This example shows how to add private commenting to your app with +[Liveblocks](https://liveblocks.io) and [Next.js](https://nextjs.org/). + +Private Comments + +## Getting started + +Run the following command to try this example locally: + +```bash +npx create-liveblocks-app@latest --example nextjs-comments-private --api-key +``` + +This will download the example and ask permission to open your browser, enabling +you to automatically get your API key from your +[liveblocks.io](https://liveblocks.io) account. + +### Manual setup + +
Read more + +

+ +Alternatively, you can set up your project manually: + +- Install all dependencies with `npm install` +- Create an account on [liveblocks.io](https://liveblocks.io/dashboard) +- Copy your **secret** key from the + [dashboard](https://liveblocks.io/dashboard/apikeys) +- Create an `.env.local` file and add your **secret** key as the + `LIVEBLOCKS_SECRET_KEY` environment variable +- Run `npm run dev` and go to [http://localhost:3000](http://localhost:3000) + +
+ +### Deploy on Vercel + +
Read more + +

+ +To both deploy on [Vercel](https://vercel.com), and run the example locally, use +the following command: + +```bash +npx create-liveblocks-app@latest --example nextjs-comments-private --vercel +``` + +This will download the example and ask permission to open your browser, enabling +you to deploy to Vercel. + +
+ +### Develop on CodeSandbox + +
Read more + +

+ +After forking +[this example](https://codesandbox.io/s/github/liveblocks/liveblocks/tree/main/examples/nextjs-comments-private) +on CodeSandbox, create the `LIVEBLOCKS_SECRET_KEY` environment variable as a +[secret](https://codesandbox.io/docs/secrets). + +
diff --git a/examples/nextjs-comments-private/liveblocks.config.ts b/examples/nextjs-comments-private/liveblocks.config.ts new file mode 100644 index 00000000000..7ecf28928f8 --- /dev/null +++ b/examples/nextjs-comments-private/liveblocks.config.ts @@ -0,0 +1,16 @@ +declare global { + interface Liveblocks { + // Custom user info set when authenticating with a secret key + UserMeta: { + id: string; + info: { + // Example properties, for useSelf, useUser, useOthers, etc. + name: string; + avatar: string; + color: string; + }; + }; + } +} + +export {}; diff --git a/examples/nextjs-comments-private/next.config.js b/examples/nextjs-comments-private/next.config.js new file mode 100644 index 00000000000..b49b11cbb39 --- /dev/null +++ b/examples/nextjs-comments-private/next.config.js @@ -0,0 +1,7 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + turbopack: { root: __dirname }, + reactStrictMode: true, +}; + +module.exports = nextConfig; diff --git a/examples/nextjs-comments-private/package-lock.json b/examples/nextjs-comments-private/package-lock.json new file mode 100644 index 00000000000..0f9477606ea --- /dev/null +++ b/examples/nextjs-comments-private/package-lock.json @@ -0,0 +1,3293 @@ +{ + "name": "@liveblocks-examples/nextjs-comments-private", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@liveblocks-examples/nextjs-comments-private", + "license": "Apache-2.0", + "dependencies": { + "@liveblocks/client": "3.21.0-rc1", + "@liveblocks/node": "3.21.0-rc1", + "@liveblocks/react": "3.21.0-rc1", + "@liveblocks/react-ui": "3.21.0-rc1", + "lucide-react": "^1.21.0", + "next": "^16.1.6", + "radix-ui": "^1.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13" + }, + "devDependencies": { + "@types/node": "^20.4.10", + "@types/react": "^18.3.27", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@juggle/resize-observer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", + "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", + "license": "Apache-2.0" + }, + "node_modules/@liveblocks/client": { + "version": "3.21.0-rc1", + "resolved": "https://registry.npmjs.org/@liveblocks/client/-/client-3.21.0-rc1.tgz", + "integrity": "sha512-HPkM4hk3nF5B9DXxYnD9moPoHKXdicE0Q42vjVLx+bhmTfqAzJWDo3oaXO8MxUd402/wsPnQrK7tGMtvwHzKaw==", + "license": "Apache-2.0", + "dependencies": { + "@liveblocks/core": "3.21.0-rc1" + } + }, + "node_modules/@liveblocks/core": { + "version": "3.21.0-rc1", + "resolved": "https://registry.npmjs.org/@liveblocks/core/-/core-3.21.0-rc1.tgz", + "integrity": "sha512-cQLc1h0u2HwW6DWSIoOcw7liMU+OX+4W8tXQ0nuoFjT4FmyrGvUkqR5z4Ffz2DqXSxG+suSi2Ib95xrJMD3/lw==", + "license": "Apache-2.0", + "peerDependencies": { + "@types/json-schema": "^7" + } + }, + "node_modules/@liveblocks/node": { + "version": "3.21.0-rc1", + "resolved": "https://registry.npmjs.org/@liveblocks/node/-/node-3.21.0-rc1.tgz", + "integrity": "sha512-bIyWUMH0ecPrfKQHvJOVY9ty573F+SoN8ojMMiO1nHJNXcj7aivMsX8buQezuYGI467YkR5KpBlCG2QWBkHHcA==", + "license": "Apache-2.0", + "dependencies": { + "@liveblocks/core": "3.21.0-rc1", + "@stablelib/base64": "^1.0.1", + "fast-sha256": "^1.3.0", + "marked": "^15.0.11", + "node-fetch": "^2.6.1" + } + }, + "node_modules/@liveblocks/react": { + "version": "3.21.0-rc1", + "resolved": "https://registry.npmjs.org/@liveblocks/react/-/react-3.21.0-rc1.tgz", + "integrity": "sha512-ei5rpMEAJTzHOy/K04DWuxu5cMeFKDmsvXhEu3XSfLy0B60Vxtu1o41UiS+bOh/dbaeMHvsJUY+cRE6XYCO5Lg==", + "license": "Apache-2.0", + "dependencies": { + "@liveblocks/client": "3.21.0-rc1", + "@liveblocks/core": "3.21.0-rc1" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "@types/react-dom": "^18 || ^19", + "react": "^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@liveblocks/react-ui": { + "version": "3.21.0-rc1", + "resolved": "https://registry.npmjs.org/@liveblocks/react-ui/-/react-ui-3.21.0-rc1.tgz", + "integrity": "sha512-s+ZJ0cpCeTMVlzxtQRlakh9X3plwvoIZl32VB3DjC+q6Jx0jCDSAlWv1YURdFyjXHsBLunx7HcRRKH92k3zhlA==", + "license": "Apache-2.0", + "dependencies": { + "@floating-ui/react-dom": "^2.1.0", + "@liveblocks/client": "3.21.0-rc1", + "@liveblocks/core": "3.21.0-rc1", + "@liveblocks/react": "3.21.0-rc1", + "frimousse": "^0.2.0", + "marked": "^15.0.11", + "radix-ui": "^1.4.0", + "slate": "^0.110.2", + "slate-history": "^0.110.3", + "slate-hyperscript": "^0.100.0", + "slate-react": "^0.110.3" + }, + "peerDependencies": { + "@types/react": "^18 || ^19", + "@types/react-dom": "^18 || ^19", + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@next/env": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.0.tgz", + "integrity": "sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.0.tgz", + "integrity": "sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.0.tgz", + "integrity": "sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.0.tgz", + "integrity": "sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.0.tgz", + "integrity": "sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.0.tgz", + "integrity": "sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.0.tgz", + "integrity": "sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.0.tgz", + "integrity": "sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.0.tgz", + "integrity": "sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.2.tgz", + "integrity": "sha512-ceTwaxc4I5IOi97DgCotl3pqiyRGvffcc0oOsE2dQYaJOFIDsDt4VWG6xEbg1QePv9QWausCEIppud/tJ1wNig==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.4.tgz", + "integrity": "sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.4.tgz", + "integrity": "sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.2.tgz", + "integrity": "sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz", + "integrity": "sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.2.tgz", + "integrity": "sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.17.tgz", + "integrity": "sha512-/YSAOdJ7YJvdn7bn5sdSx2egW+SKY+u7O5RyAVs94Ymrg2fg5QTSFPMRkzvhGyFuE4/qsmPBdrwYoZMZh/4f+g==", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", + "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", + "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", + "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-arrow": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", + "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-portal": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", + "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.1.tgz", + "integrity": "sha512-55bQtCnOB0BohomSHi6qvQXpJEEqUGDm6hRrM0Bph5OXwhSegqkd8IqgBAQkM1IlgUlWZIxpxRcpOEfRIgimyw==", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz", + "integrity": "sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz", + "integrity": "sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz", + "integrity": "sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.2.tgz", + "integrity": "sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz", + "integrity": "sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz", + "integrity": "sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.2.tgz", + "integrity": "sha512-IGBQPtRFdhN6MQ8dbegVmBq1LVZluya3F1jWY+puIcQC3MHctRwTDSBWCkL/3ZcnMJLTMJ++Z+ktmvg0F89iCw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz", + "integrity": "sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz", + "integrity": "sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.2.tgz", + "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", + "license": "MIT" + }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT", + "peer": true + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/direction": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", + "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/frimousse": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/frimousse/-/frimousse-0.2.0.tgz", + "integrity": "sha512-viSrsVQWKR4Q7xzC0lkx3Wu9i1+IHrth0QXn0nlIIJXpltwUnjkGXSTuoW7WHI5aJ4z49WR8E/pyQizFjlNtTA==", + "license": "MIT", + "workspaces": [ + ".", + "site" + ], + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/is-hotkey": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.2.0.tgz", + "integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==", + "license": "MIT" + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.0.tgz", + "integrity": "sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==", + "license": "MIT", + "dependencies": { + "@next/env": "16.2.0", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.2.0", + "@next/swc-darwin-x64": "16.2.0", + "@next/swc-linux-arm64-gnu": "16.2.0", + "@next/swc-linux-arm64-musl": "16.2.0", + "@next/swc-linux-x64-gnu": "16.2.0", + "@next/swc-linux-x64-musl": "16.2.0", + "@next/swc-win32-arm64-msvc": "16.2.0", + "@next/swc-win32-x64-msvc": "16.2.0", + "sharp": "^0.34.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/radix-ui": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.6.0.tgz", + "integrity": "sha512-EUEC70O03EgxWMP5aoqfBZ6iLC5bczFagGy7zhSYRt8o5DP7IWNiP3ywetse3L9b8843ExB0OGWZvgbYVJuNeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-accessible-icon": "1.1.10", + "@radix-ui/react-accordion": "1.2.14", + "@radix-ui/react-alert-dialog": "1.1.17", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-aspect-ratio": "1.1.10", + "@radix-ui/react-avatar": "1.2.0", + "@radix-ui/react-checkbox": "1.3.5", + "@radix-ui/react-collapsible": "1.1.14", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-context-menu": "2.3.1", + "@radix-ui/react-dialog": "1.1.17", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-dropdown-menu": "2.1.18", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-form": "0.1.10", + "@radix-ui/react-hover-card": "1.1.17", + "@radix-ui/react-label": "2.1.10", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-menubar": "1.1.18", + "@radix-ui/react-navigation-menu": "1.2.16", + "@radix-ui/react-one-time-password-field": "0.1.10", + "@radix-ui/react-password-toggle-field": "0.1.5", + "@radix-ui/react-popover": "1.1.17", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-progress": "1.1.10", + "@radix-ui/react-radio-group": "1.4.1", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-scroll-area": "1.2.12", + "@radix-ui/react-select": "2.3.1", + "@radix-ui/react-separator": "1.1.10", + "@radix-ui/react-slider": "1.4.1", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-switch": "1.3.1", + "@radix-ui/react-tabs": "1.1.15", + "@radix-ui/react-toast": "1.2.17", + "@radix-ui/react-toggle": "1.1.12", + "@radix-ui/react-toggle-group": "1.1.13", + "@radix-ui/react-toolbar": "1.1.13", + "@radix-ui/react-tooltip": "1.2.10", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-escape-keydown": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.10.tgz", + "integrity": "sha512-TraSwZUqTcVbiDV2/RXzAXC7aeVVXchq0daPFZE7zAxYFaMzjOUggLOfQH9KFLgRizuwVKZO/crveV1eeO3/ZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-accordion": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.14.tgz", + "integrity": "sha512-iE8YB9nmTBH8zd73ofBISZ8JCzgMoMkATJr7qDwa6u5F1+7mTM81V6fa71jgZ65rpjVpecDf1vSnwIFP9Ly1zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collapsible": "1.1.14", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.17.tgz", + "integrity": "sha512-563ygGeyWPrxyVCNp7OV4rE2aIXhFPknpFyo4wbDlcyMMPZ6ySh+zC5WTvY0ZFLgPTg/QB6tA8PyDQyJ2b4cPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dialog": "1.1.17", + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-arrow": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.10.tgz", + "integrity": "sha512-j2VTDz1vgCsmuG0k5lBfOcM8n5JPFqZBcMryasFjHYMhwxYL5SRUV5lMSUpRdNtw3D/Sv8pzJtrlAgkssYSsQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.10.tgz", + "integrity": "sha512-kbI7NrqhDeuytYrq7JjAsoXczvL8wgj2tc1MyaYWm+50bMKHCHQtVWCryslx4cCpmCTTkBcwQckE4CmmGV2haQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-avatar": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.0.tgz", + "integrity": "sha512-am/CwltXtmtdtP+5FbYblYDnMa/zuKcMJP1i3/SJMDXXfj2mG+BTqLH2wucqeyyiQMursUtg/5cK+Nh2pCaSOA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-checkbox": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.5.tgz", + "integrity": "sha512-pREzrmNnVwGvYaBoM64huTRK7B3lrTRuwj8A9nwhPiEtMb+yudiWh6zWAqEtP0Dzd5+iBa1Ki7V1pCxV8ExMdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-collapsible": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.14.tgz", + "integrity": "sha512-9bT+FvifX1FK2Mj6UEsTdyu0cN3JaA3KdfhaBao+ONrYFy/pyOy3TU1TNw7iOk1o+0hOEq67RojlUUmoFGwxyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-collection": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.10.tgz", + "integrity": "sha512-IVVz4EvBcKjrzKgof714qDnz/SzQAkLA2Emh5edlHbgcE6fNd3Un6CJLlaYcnm8N4JmAtzQgse4dOKxcD2yc9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-context-menu": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.3.1.tgz", + "integrity": "sha512-XbrxS68W5dyiE4fAb96yvJwSVU5x66B20A99sD5Mk3xSWK/LqeOnx6TZnim1KieMjXS/CTFq8reOAjWxas2G8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-dialog": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.17.tgz", + "integrity": "sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.13.tgz", + "integrity": "sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-escape-keydown": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.18.tgz", + "integrity": "sha512-PZGV82gFk0WltDRI//SsG28ZIjlo9ANTmoNYg0jLNzXXiDsAy5PkOOYQaVD1pPxY6t7gxffb1QMD6qaUvsBZdw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.10.tgz", + "integrity": "sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-form": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.10.tgz", + "integrity": "sha512-1NfuvctVtX4sU3Mmq/IdrR8UunxiCMiVg3A5UENKhFzxUBeOyaQQ+lmaQaV7Tc8cqvBKsJL3/KGBsixK0D8WFg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-label": "2.1.10", + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-hover-card": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.17.tgz", + "integrity": "sha512-GjZQIEANVkuuWeztlKz6QEHe31ZX2iDfHzcTMCQVZXC0JyQrgfKWSC+LOOEw6aVV64zyjzobIzSA4AU4eKWrHA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-label": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.10.tgz", + "integrity": "sha512-ib0zvq2ZsAqKm5tRnqGJn3vOxSgIts5ToxsXT0q1S/GfLD1Zj7UOEnkw8u2w6sRmn47djpQWuSU1DCL1R29/yw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-menu": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.18.tgz", + "integrity": "sha512-lj8Rxjtn6zJq1oSbE/uDtAwCbB9BnxgHD+8MwJMuTh6u1dPamYhW9iuELr/Z8d0D/UysFblYYHeBPwi7T4k0YQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-menubar": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.18.tgz", + "integrity": "sha512-hX7EGx/oFq6DPY27GQuP/2wP48GHf5LG6r06VgNJlG+znmDS8OfopZcRcGly3L4lsB9FqpmLx6JQSE9P3BUpyw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-menu": "2.1.18", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.16.tgz", + "integrity": "sha512-nJ0SkrSQgudyYhMiYeHA1ayLVuduEJCFLan1RZZN7c9kqzzCFLaU9kuy81uNtqzweM9YaQPgWzxi9MwQ9jZ04g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-one-time-password-field": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.10.tgz", + "integrity": "sha512-GHkcJ+WVj91At+OvUVTD4R3W0/wxw9t/sG5xFUBYXaCbtWiooZX5Md376QjJqgH4VsVyXrbVNHO2O4NYcmjfVg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-password-toggle-field": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.5.tgz", + "integrity": "sha512-fVuA82u0b/fClpbEJv8yp1nU9eSvoSEOERsU/hhf3FXGPIvkmE7oEaHEu8poowoXO39/Va7zq2E0TUcYr1dBRg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-effect-event": "0.0.3", + "@radix-ui/react-use-is-hydrated": "0.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-popper": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.1.tgz", + "integrity": "sha512-bhnq/0DEPTi2lsOD3J5rTL65qUKHbKbhqHsmN9TMiclSXpipi651ooUKPPp6G5lF/WiHBdn1s0Wuqsn+myVAvw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-rect": "1.1.2", + "@radix-ui/react-use-size": "1.1.2", + "@radix-ui/rect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-portal": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.12.tgz", + "integrity": "sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-presence": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.6.tgz", + "integrity": "sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-primitive": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.6.tgz", + "integrity": "sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-progress": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.10.tgz", + "integrity": "sha512-JYzEg60lk79PwKM27WZyKd7PW8O4OM5jOaFfRPfOyeXmMw7tLJh5kSj+CEjVTehszuwml/AdCzPGMXBTGf4BBw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-radio-group": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.4.1.tgz", + "integrity": "sha512-/SSxZdKEo2Eo29FFRKd06EfFDYp8HryKg0WYg7QLXaydPzl52YfSvCH2a3QDBRdtcuwACroJT8UVjQVgOJ7P9A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.13.tgz", + "integrity": "sha512-9gkwneI0guf8JDmrFxPjJF6Ozzgioyw+/lonYNCwefS9ZHA05er0BVHiXr+LbWGHxUfczvMY6G1oiZZi1VzjRw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.12.tgz", + "integrity": "sha512-xuafVzQiTCLsyEjakowTdG3OgTXsmO7IdCiO77otIa+z44xoLNs9Do5eg7POFumIOCjtG6djfm6RKUKpUa/csA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-select": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.1.tgz", + "integrity": "sha512-w6eDvY78LE9ZUiNnXCA1QVK8RYN7k9galFv09kjVydJqBAgHd7Y9A6h0UJ/6DCZNGZMZrB2ohcSW1Bo9d8+wWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-focus-guards": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.10", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-separator": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.10.tgz", + "integrity": "sha512-Y6K6jLQCVfCnTL2MEtGxDLffkhNfEfHsEg3Wa8JU+IWdn3EWbLXd3OuOfQRN7p/W/cUce1WyTk3QeuAoDBzN9g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-slider": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.4.1.tgz", + "integrity": "sha512-r91WSpQucNGFKAIxT8FT0H0zyjd5tJlqObLp7LOMV4z49KoDCwjy01w3vDOU4e1wxhF9IgjYco7SB6byOW7Buw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.2", + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-use-previous": "1.1.2", + "@radix-ui/react-use-size": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-tabs": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.15.tgz", + "integrity": "sha512-kxc9gI6/HfcU4nfMMVS3AmQK414kbU1IE6UCJmMmxjhO3cRPXOyYnmvyKD+ODt7q56nRq9l7Wovi6uaGwKgMlg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-toast": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.17.tgz", + "integrity": "sha512-uL4kyyWy000pPL43fGGCV5qT6ZchCWEQZOSlkYiPwPt8Hy1iW38RjeptIvz1/SZesrW6Vn58Ct3sV7tfEfiAbw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-collection": "1.1.10", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-callback-ref": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-use-layout-effect": "1.1.2", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-toggle": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.12.tgz", + "integrity": "sha512-AsAVsYNZIlRBsci7BhE+QyQeKd1h6TffJYt+lF0QQkd5OpQ3klfIByPsCb4G0h/Fq6PJwh1FYNluzBFYzhk4+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.13.tgz", + "integrity": "sha512-Xb9PLtlvU66F36LiKba6dFswu6V2mDkgidO4fNSbQHQwmZ9ObxMIO17MN/LJ4aWJecVuSVLAHPZjyeMzJrgeiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-toggle": "1.1.12", + "@radix-ui/react-use-controllable-state": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-toolbar": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.13.tgz", + "integrity": "sha512-Za1l4f6fzTkGgz/iynAMN8iaqiKff2wm2/QwiLmHPtDQreWEBrvSimgQFIekxMUdRPhILM7xdIXxuS/o/DGZag==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-direction": "1.1.2", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-roving-focus": "1.1.13", + "@radix-ui/react-separator": "1.1.10", + "@radix-ui/react-toggle-group": "1.1.13" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-tooltip": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.10.tgz", + "integrity": "sha512-NlNe8D0dWEpVfXFli90IO6X07Josx/b1iu98tDnx9Xv0HT4wLIL+m2VOheMHhK7qbp2HoTBqALEFzGyZs/levw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.3", + "@radix-ui/react-context": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.13", + "@radix-ui/react-id": "1.1.2", + "@radix-ui/react-popper": "1.3.1", + "@radix-ui/react-portal": "1.1.12", + "@radix-ui/react-presence": "1.1.6", + "@radix-ui/react-primitive": "2.1.6", + "@radix-ui/react-slot": "1.3.0", + "@radix-ui/react-use-controllable-state": "1.2.3", + "@radix-ui/react-visually-hidden": "1.2.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/radix-ui/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.6.tgz", + "integrity": "sha512-jCE0WljWifTI4niIMCll06kGpsJTAPiZVU9H4WR1N6qW7At9ystHbN7dDB+we2xH535roFHj7qKS+RGj0FMDWQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.6" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/slate": { + "version": "0.110.2", + "resolved": "https://registry.npmjs.org/slate/-/slate-0.110.2.tgz", + "integrity": "sha512-4xGULnyMCiEQ0Ml7JAC1A6HVE6MNpPJU7Eq4cXh1LxlrR0dFXC3XC+rNfQtUJ7chHoPkws57x7DDiWiZAt+PBA==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "is-plain-object": "^5.0.0", + "tiny-warning": "^1.0.3" + } + }, + "node_modules/slate-history": { + "version": "0.110.3", + "resolved": "https://registry.npmjs.org/slate-history/-/slate-history-0.110.3.tgz", + "integrity": "sha512-sgdff4Usdflmw5ZUbhDkxFwCBQ2qlDKMMkF93w66KdV48vHOgN2BmLrf+2H8SdX8PYIpP/cTB0w8qWC2GwhDVA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/slate-hyperscript": { + "version": "0.100.0", + "resolved": "https://registry.npmjs.org/slate-hyperscript/-/slate-hyperscript-0.100.0.tgz", + "integrity": "sha512-fb2KdAYg6RkrQGlqaIi4wdqz3oa0S4zKNBJlbnJbNOwa23+9FLD6oPVx9zUGqCSIpy+HIpOeqXrg0Kzwh/Ii4A==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^5.0.0" + }, + "peerDependencies": { + "slate": ">=0.65.3" + } + }, + "node_modules/slate-react": { + "version": "0.110.3", + "resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.110.3.tgz", + "integrity": "sha512-AS8PPjwmsFS3Lq0MOEegLVlFoxhyos68G6zz2nW4sh3WeTXV7pX0exnwtY1a/docn+J3LGQO11aZXTenPXA/kg==", + "license": "MIT", + "dependencies": { + "@juggle/resize-observer": "^3.4.0", + "direction": "^1.0.4", + "is-hotkey": "^0.2.0", + "is-plain-object": "^5.0.0", + "lodash": "^4.17.21", + "scroll-into-view-if-needed": "^3.1.0", + "tiny-invariant": "1.3.1" + }, + "peerDependencies": { + "react": ">=18.2.0", + "react-dom": ">=18.2.0", + "slate": ">=0.99.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/examples/nextjs-comments-private/package.json b/examples/nextjs-comments-private/package.json new file mode 100644 index 00000000000..9e2e0099f7d --- /dev/null +++ b/examples/nextjs-comments-private/package.json @@ -0,0 +1,29 @@ +{ + "name": "@liveblocks-examples/nextjs-comments-private", + "description": "This example shows how to add private commenting to your app with Liveblocks and Next.js.", + "license": "Apache-2.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@liveblocks/client": "3.21.0-rc1", + "@liveblocks/node": "3.21.0-rc1", + "@liveblocks/react": "3.21.0-rc1", + "@liveblocks/react-ui": "3.21.0-rc1", + "lucide-react": "^1.21.0", + "next": "^16.1.6", + "radix-ui": "^1.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^4.0.13" + }, + "devDependencies": { + "@types/node": "^20.4.10", + "@types/react": "^18.3.27", + "prettier": "^3.3.3", + "typescript": "^5.4.5" + } +} diff --git a/examples/nextjs-comments-private/src/app/Providers.tsx b/examples/nextjs-comments-private/src/app/Providers.tsx new file mode 100644 index 00000000000..38802f12572 --- /dev/null +++ b/examples/nextjs-comments-private/src/app/Providers.tsx @@ -0,0 +1,92 @@ +"use client"; + +import { LiveblocksProvider } from "@liveblocks/react"; +import { PropsWithChildren, Suspense } from "react"; +import { getRandomUser, getUser } from "@/database"; +import { + getUserType, + USER_ID_SEARCH_PARAM, + USER_SEARCH_PARAM, + type UserType, +} from "@/user"; + +const USER_ID_STORAGE_KEY = "liveblocks:examples:nextjs-comments-private:user"; + +async function authEndpoint(room?: string) { + const searchParams = new URLSearchParams(); + + if (typeof window !== "undefined") { + const userType = getUserType(new URLSearchParams(window.location.search)); + searchParams.set(USER_SEARCH_PARAM, userType); + searchParams.set(USER_ID_SEARCH_PARAM, getCurrentUserId(userType)); + } + + const queryString = searchParams.toString(); + const response = await fetch( + `/api/liveblocks-auth${queryString ? `?${queryString}` : ""}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ room }), + } + ); + + if (!response.ok) { + throw new Error("Problem authenticating"); + } + + return await response.json(); +} + +function getCurrentUserId(userType: UserType) { + const storageKey = `${USER_ID_STORAGE_KEY}:${userType}`; + const storedUserId = window.localStorage.getItem(storageKey); + const storedUser = storedUserId ? getUser(storedUserId) : null; + + if (storedUser?.type === userType) { + return storedUser.id; + } + + const user = getRandomUser(userType); + window.localStorage.setItem(storageKey, user.id); + return user.id; +} + +export function Providers({ children }: PropsWithChildren) { + return ( + { + const searchParams = new URLSearchParams( + userIds.map((userId) => ["userIds", userId]) + ); + const response = await fetch(`/api/users?${searchParams}`); + + if (!response.ok) { + throw new Error("Problem resolving users"); + } + + const users = await response.json(); + return users; + }} + // Find a list of users that match the current search term + resolveMentionSuggestions={async ({ text }) => { + const response = await fetch( + `/api/users/search?text=${encodeURIComponent(text)}` + ); + + if (!response.ok) { + throw new Error("Problem resolving mention suggestions"); + } + + const userIds = await response.json(); + return userIds; + }} + > + {children} + + ); +} diff --git a/examples/nextjs-comments-private/src/app/api/liveblocks-auth/route.ts b/examples/nextjs-comments-private/src/app/api/liveblocks-auth/route.ts new file mode 100644 index 00000000000..3ea248affe9 --- /dev/null +++ b/examples/nextjs-comments-private/src/app/api/liveblocks-auth/route.ts @@ -0,0 +1,47 @@ +import { Liveblocks } from "@liveblocks/node"; +import { getRandomUser, getUser } from "@/database"; +import { NextRequest, NextResponse } from "next/server"; +import { EXTERNAL_USER_TYPE, getUserType, USER_ID_SEARCH_PARAM } from "@/user"; + +/** + * Authenticating your Liveblocks application + * https://liveblocks.io/docs/authentication + */ + +const liveblocks = new Liveblocks({ + secret: process.env.LIVEBLOCKS_SECRET_KEY!, +}); + +export async function POST(request: NextRequest) { + if (!process.env.LIVEBLOCKS_SECRET_KEY) { + return new NextResponse("Missing LIVEBLOCKS_SECRET_KEY", { status: 403 }); + } + + const userType = getUserType(request.nextUrl.searchParams); + + // Get the current user's unique id and info from your database + const requestedUserId = + request.nextUrl.searchParams.get(USER_ID_SEARCH_PARAM); + const requestedUser = requestedUserId ? getUser(requestedUserId) : null; + const user = + requestedUser?.type === userType ? requestedUser : getRandomUser(userType); + + // Create a session for the current user (access token auth) + // userInfo is made available in Liveblocks user hooks, e.g. useSelf + const session = liveblocks.prepareSession(user.id, { + userInfo: user.info, + }); + + // Use a naming pattern to allow access to rooms with a wildcard + session.allow( + `liveblocks:examples:*`, + // External users don't have access to private comments + userType === EXTERNAL_USER_TYPE + ? ["*:write", "comments:private:none"] + : ["*:write"] + ); + + // Authorize the user and return the result + const { status, body } = await session.authorize(); + return new NextResponse(body, { status }); +} diff --git a/examples/nextjs-comments-private/src/app/api/users/route.ts b/examples/nextjs-comments-private/src/app/api/users/route.ts new file mode 100644 index 00000000000..77c2fd40e7f --- /dev/null +++ b/examples/nextjs-comments-private/src/app/api/users/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUser } from "@/database"; + +/** + * Get users' info from their ID + * For `resolveUsers` in liveblocks.config.ts + */ + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const userIds = searchParams.getAll("userIds"); + + if (!userIds || !Array.isArray(userIds)) { + return new NextResponse("Missing or invalid userIds", { status: 400 }); + } + + return NextResponse.json( + userIds.map((userId) => getUser(userId)?.info || null) + ); +} diff --git a/examples/nextjs-comments-private/src/app/api/users/search/route.ts b/examples/nextjs-comments-private/src/app/api/users/search/route.ts new file mode 100644 index 00000000000..07bfc3ba726 --- /dev/null +++ b/examples/nextjs-comments-private/src/app/api/users/search/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getUsers } from "@/database"; + +/** + * Returns a list of user IDs from a partial search input + * For `resolveMentionSuggestions` in liveblocks.config.ts + */ + +export async function GET(request: NextRequest) { + const { searchParams } = new URL(request.url); + const text = searchParams.get("text"); + + const filteredUserIds = getUsers() + .filter((user) => + text ? user.info.name.toLowerCase().includes(text.toLowerCase()) : true + ) + .map((user) => user.id); + + return NextResponse.json(filteredUserIds); +} diff --git a/examples/nextjs-comments-private/src/app/layout.tsx b/examples/nextjs-comments-private/src/app/layout.tsx new file mode 100644 index 00000000000..2919d34ea87 --- /dev/null +++ b/examples/nextjs-comments-private/src/app/layout.tsx @@ -0,0 +1,33 @@ +import "../styles/globals.css"; +import { Providers } from "./Providers"; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + Liveblocks + + + + + + + {children} + + + ); +} diff --git a/examples/nextjs-comments-private/src/app/page.tsx b/examples/nextjs-comments-private/src/app/page.tsx new file mode 100644 index 00000000000..031c1424c5b --- /dev/null +++ b/examples/nextjs-comments-private/src/app/page.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { EyeOffIcon } from "lucide-react"; +import { Switch } from "radix-ui"; +import { + RoomProvider, + useSelf, + useThreads, + useUser, +} from "@liveblocks/react/suspense"; +import { Loading } from "../components/Loading"; +import { Composer, Thread } from "@liveblocks/react-ui"; +import { ClientSideSuspense } from "@liveblocks/react"; +import { ErrorBoundary } from "react-error-boundary"; +import { + EXTERNAL_USER_TYPE, + getUserType, + INTERNAL_USER_TYPE, + USER_SEARCH_PARAM, + type UserType, +} from "@/user"; + +function Example({ + userType, + onUserTypeChange, +}: { + userType: UserType; + onUserTypeChange: (userType: UserType) => void; +}) { + const isInternalUser = userType === INTERNAL_USER_TYPE; + const { threads } = useThreads(); + const [isPrivateThread, setPrivateThread] = useState(false); + const isComposerPrivate = isInternalUser && isPrivateThread; + + return ( + <> +
+ + + {threads.map((thread) => { + return ( +
+ {thread.visibility === "private" ? ( +
+ + Not visible to external users +
+ ) : null} + +
+ ); + })} + +
+ + {isInternalUser ? ( +
+ + + + + + It won't be visible to external users + +
+ ) : null} +
+
+ + ); +} + +function UserCard({ + userType, + onUserTypeChange, +}: { + userType: UserType; + onUserTypeChange: (userType: UserType) => void; +}) { + const userId = useSelf((me) => me.id); + const { user } = useUser(userId); + const isInternalUser = userType === INTERNAL_USER_TYPE; + + return ( +
+
+ +
+
{user.name}
+
+ {isInternalUser ? "Internal user" : "External user"} +
+
+
+ +
+ ); +} + +export default function Page() { + const params = useSearchParams(); + const userType = getUserType(params); + const roomId = useExampleRoomId( + "liveblocks:examples:nextjs-comments-private" + ); + const setUserType = useCallback( + (nextUserType: UserType) => { + if (nextUserType === userType) { + return; + } + + const url = new URL(window.location.href); + url.searchParams.set(USER_SEARCH_PARAM, nextUserType); + window.location.assign(url.toString()); + }, + [userType] + ); + + return ( + + There was an error while getting threads. + } + > + }> + + + + + ); +} + +/** + * This function is used when deploying an example on liveblocks.io. + * You can ignore it completely if you run the example locally. + */ +function useExampleRoomId(roomId: string) { + const params = useSearchParams(); + const exampleId = params?.get("exampleId"); + + const exampleRoomId = useMemo(() => { + return exampleId ? `${roomId}-${exampleId}` : roomId; + }, [roomId, exampleId]); + + return exampleRoomId; +} diff --git a/examples/nextjs-comments-private/src/components/Loading.tsx b/examples/nextjs-comments-private/src/components/Loading.tsx new file mode 100644 index 00000000000..1d604ac02d5 --- /dev/null +++ b/examples/nextjs-comments-private/src/components/Loading.tsx @@ -0,0 +1,7 @@ +export function Loading() { + return ( +
+ Loading +
+ ); +} diff --git a/examples/nextjs-comments-private/src/database.ts b/examples/nextjs-comments-private/src/database.ts new file mode 100644 index 00000000000..349c2f2eb0b --- /dev/null +++ b/examples/nextjs-comments-private/src/database.ts @@ -0,0 +1,93 @@ +import { EXTERNAL_USER_TYPE, INTERNAL_USER_TYPE, type UserType } from "@/user"; + +type ExampleUser = Liveblocks["UserMeta"] & { + type: UserType; +}; + +const USER_INFO: ExampleUser[] = [ + { + id: "charlie.layne@example.com", + type: INTERNAL_USER_TYPE, + info: { + name: "Charlie Layne", + color: "#D583F0", + avatar: "https://liveblocks.io/avatars/avatar-1.png", + }, + }, + { + id: "mislav.abha@example.com", + type: INTERNAL_USER_TYPE, + info: { + name: "Mislav Abha", + color: "#F08385", + avatar: "https://liveblocks.io/avatars/avatar-2.png", + }, + }, + { + id: "tatum.paolo@example.com", + type: INTERNAL_USER_TYPE, + info: { + name: "Tatum Paolo", + color: "#F0D885", + avatar: "https://liveblocks.io/avatars/avatar-3.png", + }, + }, + { + id: "anjali.wanda@example.com", + type: INTERNAL_USER_TYPE, + info: { + name: "Anjali Wanda", + color: "#85EED6", + avatar: "https://liveblocks.io/avatars/avatar-4.png", + }, + }, + { + id: "jody.hekla@example.com", + type: EXTERNAL_USER_TYPE, + info: { + name: "Jody Hekla", + color: "#85BBF0", + avatar: "https://liveblocks.io/avatars/avatar-5.png", + }, + }, + { + id: "emil.joyce@example.com", + type: EXTERNAL_USER_TYPE, + info: { + name: "Emil Joyce", + color: "#8594F0", + avatar: "https://liveblocks.io/avatars/avatar-6.png", + }, + }, + { + id: "jory.quispe@example.com", + type: EXTERNAL_USER_TYPE, + info: { + name: "Jory Quispe", + color: "#85DBF0", + avatar: "https://liveblocks.io/avatars/avatar-7.png", + }, + }, + { + id: "quinn.elton@example.com", + type: EXTERNAL_USER_TYPE, + info: { + name: "Quinn Elton", + color: "#87EE85", + avatar: "https://liveblocks.io/avatars/avatar-8.png", + }, + }, +]; + +export function getRandomUser(type: UserType) { + const users = USER_INFO.filter((user) => user.type === type); + return users[Math.floor(Math.random() * users.length)]; +} + +export function getUser(id: string) { + return USER_INFO.find((u) => u.id === id) || null; +} + +export function getUsers() { + return USER_INFO; +} diff --git a/examples/nextjs-comments-private/src/styles/globals.css b/examples/nextjs-comments-private/src/styles/globals.css new file mode 100644 index 00000000000..0b795d9f626 --- /dev/null +++ b/examples/nextjs-comments-private/src/styles/globals.css @@ -0,0 +1,264 @@ +@import "@liveblocks/react-ui/styles.css"; +@import "@liveblocks/react-ui/styles/dark/media-query.css"; + +html, +body { + background: #f3f3f3; + padding: 0; + margin: 0; + font-family: + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Roboto, + Oxygen, + Ubuntu, + Cantarell, + Fira Sans, + Droid Sans, + Helvetica Neue, + sans-serif; +} + +* { + box-sizing: border-box; +} + +.lb-root { + --lb-accent: #44f; +} + +main { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 4rem 1rem; + margin: 0 auto; + max-width: 680px; +} + +.loading, +.error { + position: absolute; + width: 100vw; + height: 100vh; + display: flex; + place-content: center; + place-items: center; +} + +.loading img { + width: 64px; + height: 64px; + opacity: 0.2; +} + +.thread, +.composer, +.user-card { + position: relative; + border-radius: 0.75rem; + overflow: hidden; + background: var(--lb-background); + box-shadow: + 0 0 0 1px rgb(0 0 0 / 4%), + 0 2px 6px rgb(0 0 0 / 4%), + 0 8px 26px rgb(0 0 0 / 6%); + transition: background var(--lb-transition-duration) + var(--lb-transition-easing); +} + +.thread[data-visibility="private"], +.composer[data-visibility="private"] { + --lb-accent: #ffba00; + --lb-background: #fff8ea; + + background: var(--lb-background); +} + +.user-card { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.875rem; +} + +.user-card-profile { + display: flex; + align-items: center; + min-width: 0; + gap: 0.75rem; +} + +.user-card-avatar { + flex: none; + width: 28px; + height: 28px; + border-radius: var(--lb-avatar-radius); +} + +.user-card-text { + min-width: 0; +} + +.user-card-name { + overflow: hidden; + font-size: 0.8125rem; + color: var(--lb-foreground); + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-card-access { + color: var(--lb-foreground-moderate); + font-size: 0.8125rem; +} + +.user-card-button { + flex: none; + margin-left: auto; + padding: 0.375rem 0.625rem; + border: 0; + border-radius: var(--lb-button-radius); + background: var(--lb-accent); + color: var(--lb-accent-foreground); + font: inherit; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; +} + +.thread-visibility, +.composer-visibility { + display: flex; + align-items: center; + gap: 0.625rem; + font-size: 0.875rem; +} + +.thread-visibility { + padding: 0.625rem 0.875rem; + border-bottom: 1px solid var(--lb-foreground-subtle); + color: var(--lb-foreground-tertiary); +} + +.thread-visibility-icon { + flex: none; + width: 16px; + height: 16px; + color: var(--lb-foreground-moderate); +} + +.composer-visibility { + justify-content: flex-start; + padding: 0.75rem 0.875rem; + border-top: 1px solid var(--lb-foreground-subtle); + color: var(--lb-foreground-secondary); + user-select: none; +} + +.composer-visibility label, +.switch { + cursor: pointer; +} + +.composer-visibility-label { + display: flex; + align-items: baseline; + flex: none; + line-height: 1.25rem; + white-space: nowrap; +} + +.composer-visibility-label-title { + font-weight: 500; + color: var(--lb-foreground-tertiary); +} + +.composer-visibility-description { + margin-left: auto; + min-width: 0; + overflow: hidden; + color: var(--lb-foreground-moderate); + line-height: 1.25rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.switch { + display: flex; + align-items: center; + width: 2.25rem; + height: 1.25rem; + padding: 0.125rem; + border: 0; + border-radius: 999px; + background: var(--lb-foreground-subtle); +} + +.composer-visibility-switch { + flex: none; +} + +.switch[data-state="checked"] { + background: var(--lb-accent); +} + +.switch[data-disabled] { + cursor: not-allowed; + opacity: 0.55; +} + +.switch-thumb { + display: block; + width: 1rem; + height: 1rem; + border-radius: 999px; + background: var(--lb-accent-foreground); + transition: transform var(--lb-transition-duration) + var(--lb-transition-easing); +} + +.switch-thumb[data-state="checked"] { + transform: translateX(1rem); +} + +@media (prefers-color-scheme: dark) { + html, + body { + background: #111; + } + + .lb-root { + --lb-accent: #77f; + } + + .loading img { + filter: invert(1); + } + + .error { + color: #fff; + } + + .thread[data-visibility="private"], + .composer[data-visibility="private"] { + --lb-accent: #ffba00; + --lb-background: #2a2316; + background: var(--lb-background); + } + + .thread::after, + .composer::after, + .user-card::after { + content: ""; + position: absolute; + width: 100%; + height: 100%; + inset: 0; + border-radius: inherit; + pointer-events: none; + box-shadow: inset 0 0 0 1px rgb(255 255 255 / 6%); + } +} diff --git a/examples/nextjs-comments-private/src/user.ts b/examples/nextjs-comments-private/src/user.ts new file mode 100644 index 00000000000..e8cb66ca96a --- /dev/null +++ b/examples/nextjs-comments-private/src/user.ts @@ -0,0 +1,19 @@ +export const USER_SEARCH_PARAM = "userType"; +export const USER_ID_SEARCH_PARAM = "userId"; + +export const INTERNAL_USER_TYPE = "internal"; +export const EXTERNAL_USER_TYPE = "external"; + +export type UserType = typeof INTERNAL_USER_TYPE | typeof EXTERNAL_USER_TYPE; + +type SearchParams = { + get(name: string): string | null; +}; + +export function getUserType( + searchParams: SearchParams | null | undefined +): UserType { + return searchParams?.get(USER_SEARCH_PARAM) === EXTERNAL_USER_TYPE + ? EXTERNAL_USER_TYPE + : INTERNAL_USER_TYPE; +} diff --git a/examples/nextjs-comments-private/tsconfig.json b/examples/nextjs-comments-private/tsconfig.json new file mode 100644 index 00000000000..d9fed622cc5 --- /dev/null +++ b/examples/nextjs-comments-private/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "baseUrl": ".", + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./src/*" + ] + } + }, + "include": [ + "next-env.d.ts", + ".next/types/**/*.ts", + "**/*.ts", + "**/*.tsx", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/nextjs-comments-private/vercel.json b/examples/nextjs-comments-private/vercel.json new file mode 100644 index 00000000000..5d7edc91130 --- /dev/null +++ b/examples/nextjs-comments-private/vercel.json @@ -0,0 +1,4 @@ +{ + "installCommand": "npm install", + "buildCommand": "npm run build" +} From 96b8735e6bedb4aa273530caa254a24a2d947295 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 29 Jun 2026 12:20:37 +0200 Subject: [PATCH 4/8] Adjust type to new shape returned by the backend for clients >= 3.21 (#3546) --- CHANGELOG.md | 4 ++ docs/pages/api-reference/liveblocks-react.mdx | 30 ++++++++++ packages/liveblocks-core/src/api-client.ts | 48 ++++++---------- .../src/protocol/VersionHistory.ts | 8 +-- packages/liveblocks-core/src/room.ts | 34 +++++------ .../history-version-preview.tsx | 4 +- .../version-history/HistoryVersionPreview.tsx | 4 +- .../liveblocks-react/scripts/check-exports.ts | 1 + .../src/__tests__/useHistoryVersions.test.tsx | 56 ++++--------------- packages/liveblocks-react/src/index.ts | 1 + packages/liveblocks-react/src/room.tsx | 41 ++++++++++---- packages/liveblocks-react/src/types/index.ts | 22 ++++---- .../liveblocks-react/src/umbrella-store.ts | 4 +- 13 files changed, 131 insertions(+), 126 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98752e253e5..9b99370f1b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ ### `@liveblocks/client` +- **Breaking:** Remove `type` and `kind` fields from `HistoryVersion` type. The + backend no longer returns these. - Add `visibility` to `createThread`. - Support querying by `visibility` in `getThreads`. @@ -20,6 +22,8 @@ - Add `visibility` to `useCreateThread`. - Support querying by `visibility` in `useThreads`. +- Add `useHistoryVersionYjsData()` hook to retrieve raw Yjs binary data for a + given version. Deprecate `useHistoryVersionData()` in its favor. ### `@liveblocks/node` diff --git a/docs/pages/api-reference/liveblocks-react.mdx b/docs/pages/api-reference/liveblocks-react.mdx index 04b5d89c12c..ca067ec5983 100644 --- a/docs/pages/api-reference/liveblocks-react.mdx +++ b/docs/pages/api-reference/liveblocks-react.mdx @@ -6436,6 +6436,36 @@ const { versions, error, isLoading } = useHistoryVersions(); +### useHistoryVersionYjsData [@badge=RoomProvider] + +Returns the raw Yjs binary data for a given version of the room, for use with +Yjs-based editors (e.g. TipTap, Lexical, BlockNote). + +```tsx +import { useHistoryVersionYjsData } from "@liveblocks/react"; + +const { data, error, isLoading } = useHistoryVersionYjsData(versionId); +``` + + + + The ID of the version to retrieve. Obtained from the `id` field of a + `HistoryVersion` returned by [`useHistoryVersions`][]. + + + + + + The raw Yjs binary data for the version, or `undefined` while loading. + + + Whether the version data is currently being loaded. + + + Any error that occurred while loading the version data. + + + ## Miscellaneous ### useUser [@badge=Both] diff --git a/packages/liveblocks-core/src/api-client.ts b/packages/liveblocks-core/src/api-client.ts index 798fabc64d8..bd6ef4c4345 100644 --- a/packages/liveblocks-core/src/api-client.ts +++ b/packages/liveblocks-core/src/api-client.ts @@ -356,7 +356,7 @@ export interface RoomHttpApi { mentionId: string; }): Promise; - getTextVersion({ + getYjsHistoryVersion({ roomId, versionId, }: { @@ -364,7 +364,7 @@ export interface RoomHttpApi { versionId: string; }): Promise; - createTextVersion({ roomId }: { roomId: string }): Promise; + createVersionHistorySnapshot({ roomId }: { roomId: string }): Promise; reportTextEditor({ roomId, @@ -376,20 +376,12 @@ export interface RoomHttpApi { rootKey: string; }): Promise; - listTextVersions({ roomId }: { roomId: string }): Promise<{ - versions: { - type: "historyVersion"; - kind: "yjs"; - id: string; - authors: { - id: string; - }[]; - createdAt: Date; - }[]; + listHistoryVersions({ roomId }: { roomId: string }): Promise<{ + versions: HistoryVersion[]; requestedAt: Date; }>; - listTextVersionsSince({ + listHistoryVersionsSince({ roomId, since, signal, @@ -398,15 +390,7 @@ export interface RoomHttpApi { since: Date; signal?: AbortSignal; }): Promise<{ - versions: { - type: "historyVersion"; - kind: "yjs"; - id: string; - authors: { - id: string; - }[]; - createdAt: Date; - }[]; + versions: HistoryVersion[]; requestedAt: Date; }>; @@ -1390,12 +1374,12 @@ export function createApiClient< ); } - async function getTextVersion(options: { + async function getYjsHistoryVersion(options: { roomId: string; versionId: string; }) { return httpClient.rawGet( - url`/v2/c/rooms/${options.roomId}/y-version/${options.versionId}`, + url`/v2/c/rooms/${options.roomId}/versions/${options.versionId}/yjs`, await authManager.getAuthValue({ roomId: options.roomId, resource: "storage", @@ -1404,9 +1388,9 @@ export function createApiClient< ); } - async function createTextVersion(options: { roomId: string }) { + async function createVersionHistorySnapshot(options: { roomId: string }) { await httpClient.rawPost( - url`/v2/c/rooms/${options.roomId}/version`, + url`/v2/c/rooms/${options.roomId}/versions`, await authManager.getAuthValue({ roomId: options.roomId, resource: "storage", @@ -1470,7 +1454,7 @@ export function createApiClient< return result.content[0].text; } - async function listTextVersions(options: { roomId: string }) { + async function listHistoryVersions(options: { roomId: string }) { const result = await httpClient.get<{ versions: DateToString[]; meta: { @@ -1496,7 +1480,7 @@ export function createApiClient< }; } - async function listTextVersionsSince(options: { + async function listHistoryVersionsSince(options: { roomId: string; since: Date; signal?: AbortSignal; @@ -1904,11 +1888,11 @@ export function createApiClient< // Room text editor createTextMention, deleteTextMention, - getTextVersion, - createTextVersion, + getYjsHistoryVersion, + createVersionHistorySnapshot, reportTextEditor, - listTextVersions, - listTextVersionsSince, + listHistoryVersions, + listHistoryVersionsSince, // Room attachments getAttachmentUrl, uploadAttachment, diff --git a/packages/liveblocks-core/src/protocol/VersionHistory.ts b/packages/liveblocks-core/src/protocol/VersionHistory.ts index f23e947793d..741572c19e7 100644 --- a/packages/liveblocks-core/src/protocol/VersionHistory.ts +++ b/packages/liveblocks-core/src/protocol/VersionHistory.ts @@ -1,9 +1,5 @@ export type HistoryVersion = { - type: "historyVersion"; - kind: "yjs"; + id: `vh_${string}`; createdAt: Date; - id: string; - authors: { - id: string; - }[]; + authors: { id: string }[]; }; diff --git a/packages/liveblocks-core/src/room.ts b/packages/liveblocks-core/src/room.ts index a9d6ca645ca..f0fd87e22ac 100644 --- a/packages/liveblocks-core/src/room.ts +++ b/packages/liveblocks-core/src/room.ts @@ -1255,17 +1255,19 @@ export type PrivateRoomApi = { createTextMention(mentionId: string, mention: MentionData): Promise; deleteTextMention(mentionId: string): Promise; - listTextVersions(): Promise<{ + + // Version History APIs + listHistoryVersions(): Promise<{ versions: HistoryVersion[]; requestedAt: Date; }>; - listTextVersionsSince(options: ListTextVersionsSinceOptions): Promise<{ + listHistoryVersionsSince(options: ListTextVersionsSinceOptions): Promise<{ versions: HistoryVersion[]; requestedAt: Date; }>; - getTextVersion(versionId: string): Promise; - createTextVersion(): Promise; + getYjsHistoryVersion(versionId: string): Promise; + createVersionHistorySnapshot(): Promise; executeContextualPrompt(options: { prompt: string; @@ -1857,24 +1859,24 @@ export function createRoom< await httpClient.reportTextEditor({ roomId, type, rootKey }); } - async function listTextVersions() { - return httpClient.listTextVersions({ roomId }); + async function listHistoryVersions() { + return httpClient.listHistoryVersions({ roomId }); } - async function listTextVersionsSince(options: ListTextVersionsSinceOptions) { - return httpClient.listTextVersionsSince({ + async function listHistoryVersionsSince(options: ListTextVersionsSinceOptions) { + return httpClient.listHistoryVersionsSince({ roomId, since: options.since, signal: options.signal, }); } - async function getTextVersion(versionId: string) { - return httpClient.getTextVersion({ roomId, versionId }); + async function getYjsHistoryVersion(versionId: string) { + return httpClient.getYjsHistoryVersion({ roomId, versionId }); } - async function createTextVersion() { - return httpClient.createTextVersion({ roomId }); + async function createVersionHistorySnapshot() { + return httpClient.createVersionHistorySnapshot({ roomId }); } async function executeContextualPrompt(options: { @@ -3806,13 +3808,13 @@ export function createRoom< // delete a text mention when using a text editor deleteTextMention, // list versions of the document - listTextVersions, + listHistoryVersions, // List versions of the document since the specified date - listTextVersionsSince, + listHistoryVersionsSince, // get a specific version - getTextVersion, + getYjsHistoryVersion, // create a version - createTextVersion, + createVersionHistorySnapshot, // execute a contextual prompt executeContextualPrompt, diff --git a/packages/liveblocks-react-lexical/src/version-history/history-version-preview.tsx b/packages/liveblocks-react-lexical/src/version-history/history-version-preview.tsx index 68bdac7ae00..0bf6eb4636e 100644 --- a/packages/liveblocks-react-lexical/src/version-history/history-version-preview.tsx +++ b/packages/liveblocks-react-lexical/src/version-history/history-version-preview.tsx @@ -11,7 +11,7 @@ import { syncYjsChangesToLexical, } from "@lexical/yjs"; import type { HistoryVersion } from "@liveblocks/core"; -import { useHistoryVersionData } from "@liveblocks/react"; +import { useHistoryVersionYjsData } from "@liveblocks/react"; import { useOverrides } from "@liveblocks/react-ui"; import { Button, @@ -112,7 +112,7 @@ export const HistoryVersionPreview = forwardRef< const [parentEditor, parentContext] = useLexicalComposerContext(); const editor = useRef(); const $ = useOverrides(); - const { isLoading, data, error } = useHistoryVersionData(version.id); + const { isLoading, data, error } = useHistoryVersionYjsData(version.id); const initialConfig = useMemo(() => { const nodes = Array.from(parentEditor._nodes.values()).map((n) => n.klass); diff --git a/packages/liveblocks-react-tiptap/src/version-history/HistoryVersionPreview.tsx b/packages/liveblocks-react-tiptap/src/version-history/HistoryVersionPreview.tsx index 6665438e7da..2d54a97ad2c 100644 --- a/packages/liveblocks-react-tiptap/src/version-history/HistoryVersionPreview.tsx +++ b/packages/liveblocks-react-tiptap/src/version-history/HistoryVersionPreview.tsx @@ -1,5 +1,5 @@ import type { HistoryVersion } from "@liveblocks/core"; -import { useHistoryVersionData } from "@liveblocks/react"; +import { useHistoryVersionYjsData } from "@liveblocks/react"; import { useOverrides } from "@liveblocks/react-ui"; import { Button, @@ -39,7 +39,7 @@ export const HistoryVersionPreview = forwardRef< forwardedRef ) => { const $ = useOverrides(); - const { isLoading, data, error } = useHistoryVersionData(version.id); + const { isLoading, data, error } = useHistoryVersionYjsData(version.id); const previewEditor = useEditor({ // ignore extensions, only get marks/nodes diff --git a/packages/liveblocks-react/scripts/check-exports.ts b/packages/liveblocks-react/scripts/check-exports.ts index 3cba834f85a..7173a2c8c3a 100755 --- a/packages/liveblocks-react/scripts/check-exports.ts +++ b/packages/liveblocks-react/scripts/check-exports.ts @@ -70,6 +70,7 @@ const CLASSIC_ONLY = [ "createLiveblocksContext", "createRoomContext", "useHistoryVersionData", + "useHistoryVersionYjsData", "useSearchComments", ]; const SUSPENSE_ONLY = []; diff --git a/packages/liveblocks-react/src/__tests__/useHistoryVersions.test.tsx b/packages/liveblocks-react/src/__tests__/useHistoryVersions.test.tsx index d8730d1f9e0..508c880c63d 100644 --- a/packages/liveblocks-react/src/__tests__/useHistoryVersions.test.tsx +++ b/packages/liveblocks-react/src/__tests__/useHistoryVersions.test.tsx @@ -1,4 +1,4 @@ -import type { HistoryVersion } from "@liveblocks/core"; +import type { HistoryVersion } from "@liveblocks/client"; import { nanoid } from "@liveblocks/core"; import { fireEvent, renderHook, screen } from "@testing-library/react"; import type { HttpResponseResolver } from "msw"; @@ -86,15 +86,9 @@ describe("useHistoryVersions", () => { const roomId = nanoid(); const versions: HistoryVersion[] = [ { - type: "historyVersion", - kind: "yjs", + id: "vh_version_1", createdAt: new Date(), - id: "version_1", - authors: [ - { - id: "user-1", - }, - ], + authors: [{ id: "user-1" }], }, ]; @@ -137,15 +131,9 @@ describe("useHistoryVersions", () => { const roomId = nanoid(); const versions: HistoryVersion[] = [ { - type: "historyVersion", - kind: "yjs", + id: "vh_version_1", createdAt: new Date(), - id: "version_1", - authors: [ - { - id: "user-1", - }, - ], + authors: [{ id: "user-1" }], }, ]; @@ -167,10 +155,8 @@ describe("useHistoryVersions", () => { umbrellaStore.historyVersions.update("room-1", [ { - type: "historyVersion", - kind: "yjs", + id: "vh_version_1", createdAt: new Date(), - id: "version_1", authors: [{ id: "user-1" }], }, ]); @@ -288,15 +274,9 @@ describe("useHistoryVersions: suspense", () => { const roomId = nanoid(); const versions: HistoryVersion[] = [ { - type: "historyVersion", - kind: "yjs", + id: "vh_version_1", createdAt: new Date(), - id: "version_1", - authors: [ - { - id: "user-1", - }, - ], + authors: [{ id: "user-1" }], }, ]; @@ -446,15 +426,9 @@ describe("useHistoryVersions: polling", () => { const roomId = nanoid(); const versions: HistoryVersion[] = [ { - type: "historyVersion", - kind: "yjs", + id: "vh_version_1", createdAt: new Date(), - id: "version_1", - authors: [ - { - id: "user-1", - }, - ], + authors: [{ id: "user-1" }], }, ]; @@ -505,15 +479,9 @@ describe("useHistoryVersions: polling", () => { expect(getHistoryVersionsSinceCount).toBe(0); versions.push({ - type: "historyVersion", - kind: "yjs", + id: "vh_version_2", createdAt: new Date(), - id: "version_2", - authors: [ - { - id: "user-2", - }, - ], + authors: [{ id: "user-2" }], }); // Wait for the first polling to occur after the initial render diff --git a/packages/liveblocks-react/src/index.ts b/packages/liveblocks-react/src/index.ts index c6d664e57e2..deafba986f9 100644 --- a/packages/liveblocks-react/src/index.ts +++ b/packages/liveblocks-react/src/index.ts @@ -75,6 +75,7 @@ export { useUpdateMyPresence, useUpdateRoomSubscriptionSettings, useHistoryVersionData, + useHistoryVersionYjsData, } from "./room"; // Export the classic (non-Suspense) versions of our hooks diff --git a/packages/liveblocks-react/src/room.tsx b/packages/liveblocks-react/src/room.tsx index 75fc2ff7504..e017b34f485 100644 --- a/packages/liveblocks-react/src/room.tsx +++ b/packages/liveblocks-react/src/room.tsx @@ -102,9 +102,9 @@ import type { FeedMessagesAsyncSuccess, FeedsAsyncResult, FeedsAsyncSuccess, - HistoryVersionDataAsyncResult, HistoryVersionsAsyncResult, HistoryVersionsAsyncSuccess, + HistoryVersionYjsDataAsyncResult, MutationContext, OmitFirstArg, RoomContextBundle, @@ -3146,11 +3146,11 @@ function useRoomSubscriptionSettingsSuspense(): [ /** * @internal */ -function useHistoryVersionData_withRoomContext( +function useHistoryVersionYjsData_withRoomContext( RoomContext: Context, versionId: string -): HistoryVersionDataAsyncResult { - const [state, setState] = useState({ +): HistoryVersionYjsDataAsyncResult { + const [state, setState] = useState({ isLoading: true, }); const room = useRoom_withRoomContext(RoomContext); @@ -3158,7 +3158,7 @@ function useHistoryVersionData_withRoomContext( setState({ isLoading: true }); const load = async () => { try { - const response = await room[kInternal].getTextVersion(versionId); + const response = await room[kInternal].getYjsHistoryVersion(versionId); const buffer = await response.arrayBuffer(); const data = new Uint8Array(buffer); setState({ @@ -3183,15 +3183,29 @@ function useHistoryVersionData_withRoomContext( } /** - * Returns the version data bianry for a given version + * @deprecated Use `useHistoryVersionYjsData(versionId)` instead. + * + * Returns the version data binary for a given version * * @example * const {data} = useHistoryVersionData(versionId); */ function useHistoryVersionData( versionId: string -): HistoryVersionDataAsyncResult { - return useHistoryVersionData_withRoomContext(GlobalRoomContext, versionId); +): HistoryVersionYjsDataAsyncResult { + return useHistoryVersionYjsData_withRoomContext(GlobalRoomContext, versionId); +} + +/** + * Returns the Yjs data for a given version of the room. + * + * @example + * const { data, isLoading, error } = useHistoryVersionYjsData(versionId); + */ +function useHistoryVersionYjsData( + versionId: string +): HistoryVersionYjsDataAsyncResult { + return useHistoryVersionYjsData_withRoomContext(GlobalRoomContext, versionId); } /** @@ -4125,10 +4139,10 @@ export function createRoomContext< return useHistoryVersionsSuspense_withRoomContext(BoundRoomContext); } - function useHistoryVersionData_withBoundRoomContext( - ...args: Parameters + function useHistoryVersionYjsData_withBoundRoomContext( + ...args: Parameters ) { - return useHistoryVersionData_withRoomContext(BoundRoomContext, ...args); + return useHistoryVersionYjsData_withRoomContext(BoundRoomContext, ...args); } function useRoomSubscriptionSettings_withBoundRoomContext() { @@ -4299,7 +4313,9 @@ export function createRoomContext< // prettier-ignore useHistoryVersions: useHistoryVersions_withBoundRoomContext as TRoomBundle["useHistoryVersions"], // prettier-ignore - useHistoryVersionData: useHistoryVersionData_withBoundRoomContext as TRoomBundle["useHistoryVersionData"], + useHistoryVersionData: useHistoryVersionYjsData_withBoundRoomContext as TRoomBundle["useHistoryVersionData"], + // prettier-ignore + useHistoryVersionYjsData: useHistoryVersionYjsData_withBoundRoomContext as TRoomBundle["useHistoryVersionYjsData"], // prettier-ignore useRoomSubscriptionSettings: useRoomSubscriptionSettings_withBoundRoomContext as TRoomBundle["useRoomSubscriptionSettings"], @@ -5076,6 +5092,7 @@ export { useHistoryVersionData, _useHistoryVersions as useHistoryVersions, _useHistoryVersionsSuspense as useHistoryVersionsSuspense, + useHistoryVersionYjsData, _useIsInsideRoom as useIsInsideRoom, useLostConnectionListener, useMarkRoomThreadAsRead, diff --git a/packages/liveblocks-react/src/types/index.ts b/packages/liveblocks-react/src/types/index.ts index 46ba5020ea8..68e9be303e4 100644 --- a/packages/liveblocks-react/src/types/index.ts +++ b/packages/liveblocks-react/src/types/index.ts @@ -358,7 +358,7 @@ export type NotificationSettingsAsyncSuccess = AsyncSuccess; // prettier-ignore export type RoomSubscriptionSettingsAsyncResult = AsyncResult; // prettier-ignore -export type HistoryVersionDataAsyncResult = AsyncResult; +export type HistoryVersionYjsDataAsyncResult = AsyncResult; export type HistoryVersionsAsyncSuccess = AsyncSuccess; // prettier-ignore export type HistoryVersionsAsyncResult = AsyncResult; // prettier-ignore @@ -1344,12 +1344,22 @@ export type RoomContextBundle< useHistoryVersions(): HistoryVersionsAsyncResult; /** + * Returns the Yjs data for a given version of the room. + * + * @example + * const { data, error, isLoading } = useHistoryVersionYjsData(version.id); + */ + useHistoryVersionYjsData(id: string): HistoryVersionYjsDataAsyncResult; + + /** + * @deprecated Use `useHistoryVersionYjsData(id)` instead. + * * (Private beta) Returns the data of a specific version of the current room. * * @example * const { data, error, isLoading } = useHistoryVersionData(version.id); */ - useHistoryVersionData(id: string): HistoryVersionDataAsyncResult; + useHistoryVersionData(id: string): HistoryVersionYjsDataAsyncResult; suspense: Resolve< RoomContextBundleCommon & @@ -1472,14 +1482,6 @@ export type RoomContextBundle< */ useHistoryVersions(): HistoryVersionsAsyncSuccess; - // /** - // * Returns the data of a specific version of the current room's history. - // * - // * @example - // * const { data } = useHistoryVersionData(version.id); - // */ - // useHistoryVersionData(versionId: string): HistoryVersionDataState; - /** * Returns the user's subscription settings for the current room * and a function to update them. diff --git a/packages/liveblocks-react/src/umbrella-store.ts b/packages/liveblocks-react/src/umbrella-store.ts index 652c49b5257..4c181158e95 100644 --- a/packages/liveblocks-react/src/umbrella-store.ts +++ b/packages/liveblocks-react/src/umbrella-store.ts @@ -1672,7 +1672,7 @@ export class UmbrellaStore { throw new Error(`Room '${roomId}' is not available on client`); } - const result = await room[kInternal].listTextVersions(); + const result = await room[kInternal].listHistoryVersions(); this.historyVersions.update(roomId, result.versions); const lastRequestedAt = @@ -2427,7 +2427,7 @@ export class UmbrellaStore { `Room with id ${roomId} is not available on client` ); - const updates = await room[kInternal].listTextVersionsSince({ + const updates = await room[kInternal].listHistoryVersionsSince({ since: lastRequestedAt, signal, }); From a982645d964fb8e089aae820a4b59ed15588c4cd Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Mon, 29 Jun 2026 10:22:36 +0000 Subject: [PATCH 5/8] Bump to 3.21.0 --- packages/liveblocks-chat-sdk-adapter/package.json | 2 +- packages/liveblocks-client/package.json | 2 +- packages/liveblocks-core/package.json | 2 +- packages/liveblocks-emails/package.json | 2 +- packages/liveblocks-node-lexical/package.json | 2 +- packages/liveblocks-node-prosemirror/package.json | 2 +- packages/liveblocks-node/package.json | 2 +- packages/liveblocks-react-blocknote/package.json | 2 +- packages/liveblocks-react-flow/package.json | 2 +- packages/liveblocks-react-lexical/package.json | 2 +- packages/liveblocks-react-tiptap/package.json | 2 +- packages/liveblocks-react-ui/package.json | 2 +- packages/liveblocks-react/package.json | 2 +- packages/liveblocks-redux/package.json | 2 +- packages/liveblocks-yjs/package.json | 2 +- packages/liveblocks-zustand/package.json | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/liveblocks-chat-sdk-adapter/package.json b/packages/liveblocks-chat-sdk-adapter/package.json index 122cd001c0e..cef5fbcef29 100644 --- a/packages/liveblocks-chat-sdk-adapter/package.json +++ b/packages/liveblocks-chat-sdk-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/chat-sdk-adapter", - "version": "3.20.1", + "version": "3.21.0", "description": "Liveblocks adapter for the Chat SDK.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-client/package.json b/packages/liveblocks-client/package.json index c366a011d6a..2e64cc975b2 100644 --- a/packages/liveblocks-client/package.json +++ b/packages/liveblocks-client/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/client", - "version": "3.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-core/package.json b/packages/liveblocks-core/package.json index eab4276b3ea..4e4a49c9005 100644 --- a/packages/liveblocks-core/package.json +++ b/packages/liveblocks-core/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/core", - "version": "3.20.1", + "version": "3.21.0", "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 3b0aa0e40f2..0fb9fb7f71b 100644 --- a/packages/liveblocks-emails/package.json +++ b/packages/liveblocks-emails/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/emails", - "version": "3.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-node-lexical/package.json b/packages/liveblocks-node-lexical/package.json index d7ec9b3acc6..3f4cb6351f6 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.20.1", + "version": "3.21.0", "description": "A server-side utility that lets you modify lexical documents hosted in Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-node-prosemirror/package.json b/packages/liveblocks-node-prosemirror/package.json index 106d920dbf3..e462cfed724 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.20.1", + "version": "3.21.0", "description": "A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-node/package.json b/packages/liveblocks-node/package.json index a6fbf9077d4..2d6d8c57703 100644 --- a/packages/liveblocks-node/package.json +++ b/packages/liveblocks-node/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/node", - "version": "3.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-react-blocknote/package.json b/packages/liveblocks-react-blocknote/package.json index ffd94c019e4..2b08ca60d6e 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.20.1", + "version": "3.21.0", "description": "An integration of BlockNote + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-react-flow/package.json b/packages/liveblocks-react-flow/package.json index 9b1eec21ea5..6194e4a5393 100644 --- a/packages/liveblocks-react-flow/package.json +++ b/packages/liveblocks-react-flow/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react-flow", - "version": "3.20.1", + "version": "3.21.0", "description": "An integration of React Flow to enable collaboration and realtime cursors with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-react-lexical/package.json b/packages/liveblocks-react-lexical/package.json index fa103380816..b8f29562481 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.20.1", + "version": "3.21.0", "description": "An integration of Lexical + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-react-tiptap/package.json b/packages/liveblocks-react-tiptap/package.json index 162500108ab..e20b5ba2be0 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.20.1", + "version": "3.21.0", "description": "An integration of TipTap + React to enable collaboration, comments, live cursors, and more with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-react-ui/package.json b/packages/liveblocks-react-ui/package.json index f25a9eaee32..eda35e477f5 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.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-react/package.json b/packages/liveblocks-react/package.json index 44cd0bd934d..4dfcb630893 100644 --- a/packages/liveblocks-react/package.json +++ b/packages/liveblocks-react/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/react", - "version": "3.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-redux/package.json b/packages/liveblocks-redux/package.json index b3b391dbef1..3b0476e71d7 100644 --- a/packages/liveblocks-redux/package.json +++ b/packages/liveblocks-redux/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/redux", - "version": "3.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-yjs/package.json b/packages/liveblocks-yjs/package.json index 0ff5163b29a..ce71c69b8a2 100644 --- a/packages/liveblocks-yjs/package.json +++ b/packages/liveblocks-yjs/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/yjs", - "version": "3.20.1", + "version": "3.21.0", "description": "Integrate your existing or new Yjs documents with Liveblocks.", "license": "Apache-2.0", "author": "Liveblocks Inc.", diff --git a/packages/liveblocks-zustand/package.json b/packages/liveblocks-zustand/package.json index 132531db23a..8148548bf32 100644 --- a/packages/liveblocks-zustand/package.json +++ b/packages/liveblocks-zustand/package.json @@ -1,6 +1,6 @@ { "name": "@liveblocks/zustand", - "version": "3.20.1", + "version": "3.21.0", "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", "author": "Liveblocks Inc.", From 6629f156068ca547f70b0f98d8887e5134fd6221 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot <> Date: Mon, 29 Jun 2026 10:27:23 +0000 Subject: [PATCH 6/8] Bump liveblocks-python to 3.21.0 --- packages/liveblocks-python-codegen/config.yaml | 2 +- packages/liveblocks-python/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/liveblocks-python-codegen/config.yaml b/packages/liveblocks-python-codegen/config.yaml index 985db9c0b8c..36b783af98f 100644 --- a/packages/liveblocks-python-codegen/config.yaml +++ b/packages/liveblocks-python-codegen/config.yaml @@ -1,7 +1,7 @@ project_name_override: liveblocks package_name_override: liveblocks -package_version_override: 3.20.0 +package_version_override: 3.21.0 post_hooks: - "uvx ruff check --fix-only ." diff --git a/packages/liveblocks-python/pyproject.toml b/packages/liveblocks-python/pyproject.toml index 6994d540bc8..d9419018413 100644 --- a/packages/liveblocks-python/pyproject.toml +++ b/packages/liveblocks-python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "liveblocks" -version = "3.20.0" +version = "3.21.0" description = "A client library for accessing Liveblocks API" authors = [] requires-python = ">=3.11" From 90520b086e18426db4bc1b13bed0529f54086b94 Mon Sep 17 00:00:00 2001 From: Vincent Driessen Date: Mon, 29 Jun 2026 12:36:27 +0200 Subject: [PATCH 7/8] Update OpenAPI spec to match new version history endpoints (#3545) Co-authored-by: Marc Bouchenoire --- docs/references/v2.openapi.json | 136 +++++++----------- packages/liveblocks-python/README.md | 20 +-- packages/liveblocks-python/README.mdx | 32 ++--- ....py => create_version_history_snapshot.py} | 12 +- ...yjs_versions.py => get_version_history.py} | 10 +- .../liveblocks/api/yjs/get_yjs_version.py | 2 +- .../liveblocks-python/liveblocks/client.py | 96 ++++++------- .../liveblocks/models/__init__.py | 20 +-- ...eate_version_history_snapshot_response.py} | 16 +-- ...version_history_snapshot_response_data.py} | 6 +- ...nse.py => get_version_history_response.py} | 20 +-- .../liveblocks/models/history_version.py | 94 ++++++++++++ ...tem.py => history_version_authors_item.py} | 8 +- .../liveblocks/models/yjs_version.py | 121 ---------------- 14 files changed, 266 insertions(+), 327 deletions(-) rename packages/liveblocks-python/liveblocks/api/yjs/{create_yjs_version.py => create_version_history_snapshot.py} (68%) rename packages/liveblocks-python/liveblocks/api/yjs/{get_yjs_versions.py => get_version_history.py} (83%) rename packages/liveblocks-python/liveblocks/models/{create_yjs_version_response.py => create_version_history_snapshot_response.py} (53%) rename packages/liveblocks-python/liveblocks/models/{create_yjs_version_response_data.py => create_version_history_snapshot_response_data.py} (79%) rename packages/liveblocks-python/liveblocks/models/{get_yjs_versions_response.py => get_version_history_response.py} (70%) create mode 100644 packages/liveblocks-python/liveblocks/models/history_version.py rename packages/liveblocks-python/liveblocks/models/{yjs_version_authors_item.py => history_version_authors_item.py} (88%) delete mode 100644 packages/liveblocks-python/liveblocks/models/yjs_version.py diff --git a/docs/references/v2.openapi.json b/docs/references/v2.openapi.json index 319111ae54d..4453e114889 100644 --- a/docs/references/v2.openapi.json +++ b/docs/references/v2.openapi.json @@ -1487,10 +1487,10 @@ }, "/rooms/{roomId}/versions": { "get": { - "summary": "Get Yjs version history", + "summary": "Get Version History", "description": "This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest.", "tags": ["Yjs"], - "operationId": "get-yjs-versions", + "operationId": "get-version-history", "parameters": [ { "name": "roomId", @@ -1530,14 +1530,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetYjsVersionsResponse" + "$ref": "#/components/schemas/GetVersionHistoryResponse" }, "examples": { "example": { "value": { "data": [ { - "type": "historyVersion", "id": "vh_abc123", "createdAt": "2024-10-15T10:30:00.000Z", "authors": [ @@ -1547,8 +1546,7 @@ { "id": "user-456" } - ], - "kind": "yjs" + ] } ], "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI0LTEwLTE1VDEwOjMwOjAwLjAwMFoifQ==" @@ -1568,14 +1566,12 @@ "$ref": "#/components/responses/404" } } - } - }, - "/rooms/{roomId}/version/{versionId}": { - "get": { - "summary": "Get Yjs document version", - "description": "This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.", + }, + "post": { + "summary": "Create version history snapshot", + "description": "This endpoint creates a new version history snapshot for the room. Currently only works for Yjs.", "tags": ["Yjs"], - "operationId": "get-yjs-version", + "operationId": "create-version-history-snapshot", "parameters": [ { "name": "roomId", @@ -1586,26 +1582,24 @@ "description": "ID of the room", "example": "my-room-id" } - }, - { - "name": "versionId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "description": "ID of the version", - "example": "vh_abc123" - } } ], "responses": { "200": { - "description": "Success. Returns the Yjs document version as a binary stream.", + "description": "Success. Returns the created version ID.", "content": { - "application/octet-stream": { + "application/json": { "schema": { - "type": "string", - "format": "binary" + "$ref": "#/components/schemas/CreateVersionHistorySnapshotResponse" + }, + "examples": { + "example": { + "value": { + "data": { + "id": "vh_abc123" + } + } + } } } } @@ -1622,12 +1616,12 @@ } } }, - "/rooms/{roomId}/version": { - "post": { - "summary": "Create Yjs version snapshot", - "description": "This endpoint creates a new version history snapshot for the room's Yjs document.", + "/rooms/{roomId}/versions/{versionId}/yjs": { + "get": { + "summary": "Get Yjs document version", + "description": "This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update.", "tags": ["Yjs"], - "operationId": "create-yjs-version", + "operationId": "get-yjs-version", "parameters": [ { "name": "roomId", @@ -1638,24 +1632,26 @@ "description": "ID of the room", "example": "my-room-id" } + }, + { + "name": "versionId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "description": "ID of the version", + "example": "vh_abc123" + } } ], "responses": { "200": { - "description": "Success. Returns the created version ID.", + "description": "Success. Returns the Yjs document version as a binary stream.", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/CreateYjsVersionResponse" - }, - "examples": { - "example": { - "value": { - "data": { - "id": "vh_abc123" - } - } - } + "type": "string", + "format": "binary" } } } @@ -7928,18 +7924,14 @@ } } }, - "YjsVersion": { - "title": "YjsVersion", + "HistoryVersion": { + "title": "HistoryVersion", "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the version" }, - "type": { - "type": "string", - "const": "historyVersion" - }, "createdAt": { "type": "string", "format": "date-time", @@ -7957,47 +7949,27 @@ } }, "description": "List of users who contributed to this version" - }, - "kind": { - "type": "string", - "const": "yjs" } }, - "required": ["id", "type", "createdAt", "kind"], + "required": ["id", "createdAt", "authors"], "example": { "id": "vh_abc123", - "type": "historyVersion", "createdAt": "2024-10-15T10:30:00.000Z", - "authors": [ - { - "id": "user-123" - }, - { - "id": "user-456" - } - ], - "kind": "yjs" + "authors": [{ "id": "user-123" }, { "id": "user-456" }] } }, - "GetYjsVersionsResponse": { - "title": "GetYjsVersionsResponse", + "GetVersionHistoryResponse": { + "title": "GetVersionHistoryResponse", "type": "object", "properties": { "nextCursor": { "description": "Cursor for pagination to get the next page of results", - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "oneOf": [{ "type": "string" }, { "type": "null" }] }, "data": { "type": "array", "items": { - "$ref": "#/components/schemas/YjsVersion" + "$ref": "#/components/schemas/HistoryVersion" } } }, @@ -8006,22 +7978,16 @@ "example": { "data": [ { - "type": "historyVersion", "id": "vh_abc123", "createdAt": "2024-10-15T10:30:00.000Z", - "authors": [ - { - "id": "user-123" - } - ], - "kind": "yjs" + "authors": [{ "id": "user-123" }] } ], "nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI0LTEwLTE1VDEwOjMwOjAwLjAwMFoifQ==" } }, - "CreateYjsVersionResponse": { - "title": "CreateYjsVersionResponse", + "CreateVersionHistorySnapshotResponse": { + "title": "CreateVersionHistorySnapshotResponse", "type": "object", "properties": { "data": { diff --git a/packages/liveblocks-python/README.md b/packages/liveblocks-python/README.md index c437eb84604..5175305e477 100644 --- a/packages/liveblocks-python/README.md +++ b/packages/liveblocks-python/README.md @@ -578,13 +578,13 @@ print(result) --- -#### `get_yjs_versions` +#### `get_version_history` This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest. **Example** ```python -result = client.get_yjs_versions( +result = client.get_version_history( room_id="my-room-id", # limit=20, # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9", @@ -602,15 +602,14 @@ print(result) --- -#### `get_yjs_version` +#### `create_version_history_snapshot` -This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. +This endpoint creates a new version history snapshot for the room. Currently only works for Yjs. **Example** ```python -result = client.get_yjs_version( +result = client.create_version_history_snapshot( room_id="my-room-id", - version_id="vh_abc123", ) print(result) ``` @@ -619,19 +618,19 @@ print(result) | Name | Type | Required | Description | |------|------|----------|-------------| | `room_id` | `str` | Yes | ID of the room | -| `version_id` | `str` | Yes | ID of the version | --- -#### `create_yjs_version` +#### `get_yjs_version` -This endpoint creates a new version history snapshot for the room's Yjs document. +This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. **Example** ```python -result = client.create_yjs_version( +result = client.get_yjs_version( room_id="my-room-id", + version_id="vh_abc123", ) print(result) ``` @@ -640,6 +639,7 @@ print(result) | Name | Type | Required | Description | |------|------|----------|-------------| | `room_id` | `str` | Yes | ID of the room | +| `version_id` | `str` | Yes | ID of the version | --- diff --git a/packages/liveblocks-python/README.mdx b/packages/liveblocks-python/README.mdx index ac2db2d8ff0..336d4e634fc 100644 --- a/packages/liveblocks-python/README.mdx +++ b/packages/liveblocks-python/README.mdx @@ -727,12 +727,12 @@ print(result) -### get_yjs_versions +### get_version_history This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest. ```python -result = client.get_yjs_versions( +result = client.get_version_history( room_id="my-room-id", # limit=20, # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9", @@ -763,14 +763,13 @@ print(result) -### get_yjs_version +### create_version_history_snapshot -This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. +This endpoint creates a new version history snapshot for the room. Currently only works for Yjs. ```python -result = client.get_yjs_version( +result = client.create_version_history_snapshot( room_id="my-room-id", - version_id="vh_abc123", ) print(result) ``` @@ -783,23 +782,17 @@ print(result) ID of the room - - ID of the version - - -### create_yjs_version +### get_yjs_version -This endpoint creates a new version history snapshot for the room's Yjs document. +This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. ```python -result = client.create_yjs_version( +result = client.get_yjs_version( room_id="my-room-id", + version_id="vh_abc123", ) print(result) ``` @@ -812,6 +805,13 @@ print(result) ID of the room + + ID of the version + + diff --git a/packages/liveblocks-python/liveblocks/api/yjs/create_yjs_version.py b/packages/liveblocks-python/liveblocks/api/yjs/create_version_history_snapshot.py similarity index 68% rename from packages/liveblocks-python/liveblocks/api/yjs/create_yjs_version.py rename to packages/liveblocks-python/liveblocks/api/yjs/create_version_history_snapshot.py index d90ad299531..612f1936f35 100644 --- a/packages/liveblocks-python/liveblocks/api/yjs/create_yjs_version.py +++ b/packages/liveblocks-python/liveblocks/api/yjs/create_version_history_snapshot.py @@ -4,7 +4,7 @@ import httpx from ... import errors -from ...models.create_yjs_version_response import CreateYjsVersionResponse +from ...models.create_version_history_snapshot_response import CreateVersionHistorySnapshotResponse def _get_kwargs( @@ -13,7 +13,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "post", - "url": "/v2/rooms/{room_id}/version".format( + "url": "/v2/rooms/{room_id}/versions".format( room_id=quote(str(room_id), safe=""), ), } @@ -21,9 +21,9 @@ def _get_kwargs( return _kwargs -def _parse_response(*, response: httpx.Response) -> CreateYjsVersionResponse: +def _parse_response(*, response: httpx.Response) -> CreateVersionHistorySnapshotResponse: if response.status_code == 200: - response_200 = CreateYjsVersionResponse.from_dict(response.json()) + response_200 = CreateVersionHistorySnapshotResponse.from_dict(response.json()) return response_200 @@ -34,7 +34,7 @@ def _sync( room_id: str, *, client: httpx.Client, -) -> CreateYjsVersionResponse: +) -> CreateVersionHistorySnapshotResponse: kwargs = _get_kwargs( room_id=room_id, ) @@ -49,7 +49,7 @@ async def _asyncio( room_id: str, *, client: httpx.AsyncClient, -) -> CreateYjsVersionResponse: +) -> CreateVersionHistorySnapshotResponse: kwargs = _get_kwargs( room_id=room_id, ) diff --git a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_versions.py b/packages/liveblocks-python/liveblocks/api/yjs/get_version_history.py similarity index 83% rename from packages/liveblocks-python/liveblocks/api/yjs/get_yjs_versions.py rename to packages/liveblocks-python/liveblocks/api/yjs/get_version_history.py index f6301771d6c..23b0ee4df97 100644 --- a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_versions.py +++ b/packages/liveblocks-python/liveblocks/api/yjs/get_version_history.py @@ -4,7 +4,7 @@ import httpx from ... import errors -from ...models.get_yjs_versions_response import GetYjsVersionsResponse +from ...models.get_version_history_response import GetVersionHistoryResponse from ...types import UNSET, Unset @@ -34,9 +34,9 @@ def _get_kwargs( return _kwargs -def _parse_response(*, response: httpx.Response) -> GetYjsVersionsResponse: +def _parse_response(*, response: httpx.Response) -> GetVersionHistoryResponse: if response.status_code == 200: - response_200 = GetYjsVersionsResponse.from_dict(response.json()) + response_200 = GetVersionHistoryResponse.from_dict(response.json()) return response_200 @@ -49,7 +49,7 @@ def _sync( client: httpx.Client, limit: int | Unset = 20, cursor: str | Unset = UNSET, -) -> GetYjsVersionsResponse: +) -> GetVersionHistoryResponse: kwargs = _get_kwargs( room_id=room_id, limit=limit, @@ -68,7 +68,7 @@ async def _asyncio( client: httpx.AsyncClient, limit: int | Unset = 20, cursor: str | Unset = UNSET, -) -> GetYjsVersionsResponse: +) -> GetVersionHistoryResponse: kwargs = _get_kwargs( room_id=room_id, limit=limit, diff --git a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py index 089b6b6a59d..a6ed26c4c9e 100644 --- a/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py +++ b/packages/liveblocks-python/liveblocks/api/yjs/get_yjs_version.py @@ -15,7 +15,7 @@ def _get_kwargs( _kwargs: dict[str, Any] = { "method": "get", - "url": "/v2/rooms/{room_id}/version/{version_id}".format( + "url": "/v2/rooms/{room_id}/versions/{version_id}/yjs".format( room_id=quote(str(room_id), safe=""), version_id=quote(str(version_id), safe=""), ), diff --git a/packages/liveblocks-python/liveblocks/client.py b/packages/liveblocks-python/liveblocks/client.py index 13bf3b81e7c..31a0c520df5 100644 --- a/packages/liveblocks-python/liveblocks/client.py +++ b/packages/liveblocks-python/liveblocks/client.py @@ -34,9 +34,9 @@ from .models.create_group_request_body import CreateGroupRequestBody from .models.create_room_request_body import CreateRoomRequestBody from .models.create_thread_request_body import CreateThreadRequestBody + from .models.create_version_history_snapshot_response import CreateVersionHistorySnapshotResponse from .models.create_web_knowledge_source_request_body import CreateWebKnowledgeSourceRequestBody from .models.create_web_knowledge_source_response import CreateWebKnowledgeSourceResponse - from .models.create_yjs_version_response import CreateYjsVersionResponse from .models.edit_comment_metadata_request_body import EditCommentMetadataRequestBody from .models.edit_comment_request_body import EditCommentRequestBody from .models.edit_thread_metadata_request_body import EditThreadMetadataRequestBody @@ -57,10 +57,10 @@ from .models.get_thread_subscriptions_response import GetThreadSubscriptionsResponse from .models.get_threads_response import GetThreadsResponse from .models.get_user_groups_response import GetUserGroupsResponse + from .models.get_version_history_response import GetVersionHistoryResponse from .models.get_web_knowledge_source_links_response import GetWebKnowledgeSourceLinksResponse from .models.get_yjs_document_response import GetYjsDocumentResponse from .models.get_yjs_document_type import GetYjsDocumentType - from .models.get_yjs_versions_response import GetYjsVersionsResponse from .models.group import Group from .models.identify_user_request_body import IdentifyUserRequestBody from .models.identify_user_response import IdentifyUserResponse @@ -926,14 +926,14 @@ def get_yjs_document_as_binary_update( client=self._client, ) - def get_yjs_versions( + def get_version_history( self, room_id: str, *, limit: int | Unset = 20, cursor: str | Unset = UNSET, - ) -> GetYjsVersionsResponse: - """Get Yjs version history + ) -> GetVersionHistoryResponse: + """Get Version History This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest. @@ -950,70 +950,70 @@ def get_yjs_versions( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - GetYjsVersionsResponse + GetVersionHistoryResponse """ - from .api.yjs import get_yjs_versions + from .api.yjs import get_version_history - return get_yjs_versions._sync( + return get_version_history._sync( room_id=room_id, limit=limit, cursor=cursor, client=self._client, ) - def get_yjs_version( + def create_version_history_snapshot( self, room_id: str, - version_id: str, - ) -> File: - """Get Yjs document version + ) -> CreateVersionHistorySnapshotResponse: + """Create version history snapshot - This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. + This endpoint creates a new version history snapshot for the room. Currently only works for Yjs. Args: room_id (str): ID of the room Example: my-room-id. - version_id (str): ID of the version Example: vh_abc123. Raises: errors.LiveblocksError: If the server returns a response with non-2xx status code. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - File + CreateVersionHistorySnapshotResponse """ - from .api.yjs import get_yjs_version + from .api.yjs import create_version_history_snapshot - return get_yjs_version._sync( + return create_version_history_snapshot._sync( room_id=room_id, - version_id=version_id, client=self._client, ) - def create_yjs_version( + def get_yjs_version( self, room_id: str, - ) -> CreateYjsVersionResponse: - """Create Yjs version snapshot + version_id: str, + ) -> File: + """Get Yjs document version - This endpoint creates a new version history snapshot for the room's Yjs document. + This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. Args: room_id (str): ID of the room Example: my-room-id. + version_id (str): ID of the version Example: vh_abc123. Raises: errors.LiveblocksError: If the server returns a response with non-2xx status code. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - CreateYjsVersionResponse + File """ - from .api.yjs import create_yjs_version + from .api.yjs import get_yjs_version - return create_yjs_version._sync( + return get_yjs_version._sync( room_id=room_id, + version_id=version_id, client=self._client, ) @@ -4061,14 +4061,14 @@ async def get_yjs_document_as_binary_update( client=self._client, ) - async def get_yjs_versions( + async def get_version_history( self, room_id: str, *, limit: int | Unset = 20, cursor: str | Unset = UNSET, - ) -> GetYjsVersionsResponse: - """Get Yjs version history + ) -> GetVersionHistoryResponse: + """Get Version History This endpoint returns a list of version history snapshots for the room's Yjs document. The versions are returned sorted by creation date, from newest to oldest. @@ -4085,70 +4085,70 @@ async def get_yjs_versions( httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - GetYjsVersionsResponse + GetVersionHistoryResponse """ - from .api.yjs import get_yjs_versions + from .api.yjs import get_version_history - return await get_yjs_versions._asyncio( + return await get_version_history._asyncio( room_id=room_id, limit=limit, cursor=cursor, client=self._client, ) - async def get_yjs_version( + async def create_version_history_snapshot( self, room_id: str, - version_id: str, - ) -> File: - """Get Yjs document version + ) -> CreateVersionHistorySnapshotResponse: + """Create version history snapshot - This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. + This endpoint creates a new version history snapshot for the room. Currently only works for Yjs. Args: room_id (str): ID of the room Example: my-room-id. - version_id (str): ID of the version Example: vh_abc123. Raises: errors.LiveblocksError: If the server returns a response with non-2xx status code. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - File + CreateVersionHistorySnapshotResponse """ - from .api.yjs import get_yjs_version + from .api.yjs import create_version_history_snapshot - return await get_yjs_version._asyncio( + return await create_version_history_snapshot._asyncio( room_id=room_id, - version_id=version_id, client=self._client, ) - async def create_yjs_version( + async def get_yjs_version( self, room_id: str, - ) -> CreateYjsVersionResponse: - """Create Yjs version snapshot + version_id: str, + ) -> File: + """Get Yjs document version - This endpoint creates a new version history snapshot for the room's Yjs document. + This endpoint returns a specific version of the room's Yjs document encoded as a binary Yjs update. Args: room_id (str): ID of the room Example: my-room-id. + version_id (str): ID of the version Example: vh_abc123. Raises: errors.LiveblocksError: If the server returns a response with non-2xx status code. httpx.TimeoutException: If the request takes longer than Client.timeout. Returns: - CreateYjsVersionResponse + File """ - from .api.yjs import create_yjs_version + from .api.yjs import get_yjs_version - return await create_yjs_version._asyncio( + return await get_yjs_version._asyncio( room_id=room_id, + version_id=version_id, client=self._client, ) diff --git a/packages/liveblocks-python/liveblocks/models/__init__.py b/packages/liveblocks-python/liveblocks/models/__init__.py index 6c498030c30..57fae14c4f6 100644 --- a/packages/liveblocks-python/liveblocks/models/__init__.py +++ b/packages/liveblocks-python/liveblocks/models/__init__.py @@ -55,11 +55,11 @@ from .create_thread_request_body import CreateThreadRequestBody from .create_thread_request_body_comment import CreateThreadRequestBodyComment from .create_thread_request_body_visibility import CreateThreadRequestBodyVisibility +from .create_version_history_snapshot_response import CreateVersionHistorySnapshotResponse +from .create_version_history_snapshot_response_data import CreateVersionHistorySnapshotResponseData from .create_web_knowledge_source_request_body import CreateWebKnowledgeSourceRequestBody from .create_web_knowledge_source_request_body_type import CreateWebKnowledgeSourceRequestBodyType from .create_web_knowledge_source_response import CreateWebKnowledgeSourceResponse -from .create_yjs_version_response import CreateYjsVersionResponse -from .create_yjs_version_response_data import CreateYjsVersionResponseData from .edit_comment_metadata_request_body import EditCommentMetadataRequestBody from .edit_comment_metadata_request_body_metadata import EditCommentMetadataRequestBodyMetadata from .edit_comment_request_body import EditCommentRequestBody @@ -86,10 +86,10 @@ from .get_thread_subscriptions_response import GetThreadSubscriptionsResponse from .get_threads_response import GetThreadsResponse from .get_user_groups_response import GetUserGroupsResponse +from .get_version_history_response import GetVersionHistoryResponse from .get_web_knowledge_source_links_response import GetWebKnowledgeSourceLinksResponse from .get_yjs_document_response import GetYjsDocumentResponse from .get_yjs_document_type import GetYjsDocumentType -from .get_yjs_versions_response import GetYjsVersionsResponse from .google_model import GoogleModel from .google_provider_options import GoogleProviderOptions from .google_provider_options_google import GoogleProviderOptionsGoogle @@ -97,6 +97,8 @@ from .group import Group from .group_member import GroupMember from .group_scopes import GroupScopes +from .history_version import HistoryVersion +from .history_version_authors_item import HistoryVersionAuthorsItem from .identify_user_request_body import IdentifyUserRequestBody from .identify_user_request_body_user_info import IdentifyUserRequestBodyUserInfo from .identify_user_response import IdentifyUserResponse @@ -175,8 +177,6 @@ from .user_subscription import UserSubscription from .web_knowledge_source_link import WebKnowledgeSourceLink from .web_knowledge_source_link_status import WebKnowledgeSourceLinkStatus -from .yjs_version import YjsVersion -from .yjs_version_authors_item import YjsVersionAuthorsItem __all__ = ( "ActiveUsersResponse", @@ -228,11 +228,11 @@ "CreateThreadRequestBody", "CreateThreadRequestBodyComment", "CreateThreadRequestBodyVisibility", + "CreateVersionHistorySnapshotResponse", + "CreateVersionHistorySnapshotResponseData", "CreateWebKnowledgeSourceRequestBody", "CreateWebKnowledgeSourceRequestBodyType", "CreateWebKnowledgeSourceResponse", - "CreateYjsVersionResponse", - "CreateYjsVersionResponseData", "EditCommentMetadataRequestBody", "EditCommentMetadataRequestBodyMetadata", "EditCommentRequestBody", @@ -259,10 +259,10 @@ "GetThreadsResponse", "GetThreadSubscriptionsResponse", "GetUserGroupsResponse", + "GetVersionHistoryResponse", "GetWebKnowledgeSourceLinksResponse", "GetYjsDocumentResponse", "GetYjsDocumentType", - "GetYjsVersionsResponse", "GoogleModel", "GoogleProviderOptions", "GoogleProviderOptionsGoogle", @@ -270,6 +270,8 @@ "Group", "GroupMember", "GroupScopes", + "HistoryVersion", + "HistoryVersionAuthorsItem", "IdentifyUserRequestBody", "IdentifyUserRequestBodyUserInfo", "IdentifyUserResponse", @@ -347,6 +349,4 @@ "UserSubscription", "WebKnowledgeSourceLink", "WebKnowledgeSourceLinkStatus", - "YjsVersion", - "YjsVersionAuthorsItem", ) diff --git a/packages/liveblocks-python/liveblocks/models/create_yjs_version_response.py b/packages/liveblocks-python/liveblocks/models/create_version_history_snapshot_response.py similarity index 53% rename from packages/liveblocks-python/liveblocks/models/create_yjs_version_response.py rename to packages/liveblocks-python/liveblocks/models/create_version_history_snapshot_response.py index 03f2177f89e..34c64909b87 100644 --- a/packages/liveblocks-python/liveblocks/models/create_yjs_version_response.py +++ b/packages/liveblocks-python/liveblocks/models/create_version_history_snapshot_response.py @@ -6,20 +6,20 @@ from attrs import define as _attrs_define if TYPE_CHECKING: - from ..models.create_yjs_version_response_data import CreateYjsVersionResponseData + from ..models.create_version_history_snapshot_response_data import CreateVersionHistorySnapshotResponseData @_attrs_define -class CreateYjsVersionResponse: +class CreateVersionHistorySnapshotResponse: """ Example: {'data': {'id': 'vh_abc123'}} Attributes: - data (CreateYjsVersionResponseData): + data (CreateVersionHistorySnapshotResponseData): """ - data: CreateYjsVersionResponseData + data: CreateVersionHistorySnapshotResponseData def to_dict(self) -> dict[str, Any]: data = self.data.to_dict() @@ -36,13 +36,13 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: - from ..models.create_yjs_version_response_data import CreateYjsVersionResponseData + from ..models.create_version_history_snapshot_response_data import CreateVersionHistorySnapshotResponseData d = dict(src_dict) - data = CreateYjsVersionResponseData.from_dict(d.pop("data")) + data = CreateVersionHistorySnapshotResponseData.from_dict(d.pop("data")) - create_yjs_version_response = cls( + create_version_history_snapshot_response = cls( data=data, ) - return create_yjs_version_response + return create_version_history_snapshot_response diff --git a/packages/liveblocks-python/liveblocks/models/create_yjs_version_response_data.py b/packages/liveblocks-python/liveblocks/models/create_version_history_snapshot_response_data.py similarity index 79% rename from packages/liveblocks-python/liveblocks/models/create_yjs_version_response_data.py rename to packages/liveblocks-python/liveblocks/models/create_version_history_snapshot_response_data.py index f958c26db07..9ae8a4a50f2 100644 --- a/packages/liveblocks-python/liveblocks/models/create_yjs_version_response_data.py +++ b/packages/liveblocks-python/liveblocks/models/create_version_history_snapshot_response_data.py @@ -7,7 +7,7 @@ @_attrs_define -class CreateYjsVersionResponseData: +class CreateVersionHistorySnapshotResponseData: """ Attributes: id (str): Unique identifier for the created version @@ -33,8 +33,8 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) id = d.pop("id") - create_yjs_version_response_data = cls( + create_version_history_snapshot_response_data = cls( id=id, ) - return create_yjs_version_response_data + return create_version_history_snapshot_response_data diff --git a/packages/liveblocks-python/liveblocks/models/get_yjs_versions_response.py b/packages/liveblocks-python/liveblocks/models/get_version_history_response.py similarity index 70% rename from packages/liveblocks-python/liveblocks/models/get_yjs_versions_response.py rename to packages/liveblocks-python/liveblocks/models/get_version_history_response.py index d7b338395ff..2144288c60f 100644 --- a/packages/liveblocks-python/liveblocks/models/get_yjs_versions_response.py +++ b/packages/liveblocks-python/liveblocks/models/get_version_history_response.py @@ -6,23 +6,23 @@ from attrs import define as _attrs_define if TYPE_CHECKING: - from ..models.yjs_version import YjsVersion + from ..models.history_version import HistoryVersion @_attrs_define -class GetYjsVersionsResponse: +class GetVersionHistoryResponse: """ Example: - {'data': [{'type': 'historyVersion', 'id': 'vh_abc123', 'createdAt': '2024-10-15T10:30:00.000Z', 'authors': - [{'id': 'user-123'}], 'kind': 'yjs'}], 'nextCursor': 'eyJjcmVhdGVkQXQiOiIyMDI0LTEwLTE1VDEwOjMwOjAwLjAwMFoifQ=='} + {'data': [{'id': 'vh_abc123', 'createdAt': '2024-10-15T10:30:00.000Z', 'authors': [{'id': 'user-123'}]}], + 'nextCursor': 'eyJjcmVhdGVkQXQiOiIyMDI0LTEwLTE1VDEwOjMwOjAwLjAwMFoifQ=='} Attributes: next_cursor (None | str): Cursor for pagination to get the next page of results - data (list[YjsVersion]): + data (list[HistoryVersion]): """ next_cursor: None | str - data: list[YjsVersion] + data: list[HistoryVersion] def to_dict(self) -> dict[str, Any]: next_cursor: None | str @@ -46,7 +46,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: - from ..models.yjs_version import YjsVersion + from ..models.history_version import HistoryVersion d = dict(src_dict) @@ -60,13 +60,13 @@ def _parse_next_cursor(data: object) -> None | str: data = [] _data = d.pop("data") for data_item_data in _data: - data_item = YjsVersion.from_dict(data_item_data) + data_item = HistoryVersion.from_dict(data_item_data) data.append(data_item) - get_yjs_versions_response = cls( + get_version_history_response = cls( next_cursor=next_cursor, data=data, ) - return get_yjs_versions_response + return get_version_history_response diff --git a/packages/liveblocks-python/liveblocks/models/history_version.py b/packages/liveblocks-python/liveblocks/models/history_version.py new file mode 100644 index 00000000000..3fe2b5afffe --- /dev/null +++ b/packages/liveblocks-python/liveblocks/models/history_version.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +import datetime +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, Self + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +from dateutil.parser import isoparse + +if TYPE_CHECKING: + from ..models.history_version_authors_item import HistoryVersionAuthorsItem + + +@_attrs_define +class HistoryVersion: + """ + Example: + {'id': 'vh_abc123', 'createdAt': '2024-10-15T10:30:00.000Z', 'authors': [{'id': 'user-123'}, {'id': + 'user-456'}]} + + Attributes: + id (str): Unique identifier for the version + created_at (datetime.datetime): ISO 8601 timestamp of when the version was created + authors (list[HistoryVersionAuthorsItem]): List of users who contributed to this version + """ + + id: str + created_at: datetime.datetime + authors: list[HistoryVersionAuthorsItem] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + id = self.id + + created_at = self.created_at.isoformat() + + authors = [] + for authors_item_data in self.authors: + authors_item = authors_item_data.to_dict() + authors.append(authors_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "id": id, + "createdAt": created_at, + "authors": authors, + } + ) + + return field_dict + + @classmethod + def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: + from ..models.history_version_authors_item import HistoryVersionAuthorsItem + + d = dict(src_dict) + id = d.pop("id") + + created_at = isoparse(d.pop("createdAt")) + + authors = [] + _authors = d.pop("authors") + for authors_item_data in _authors: + authors_item = HistoryVersionAuthorsItem.from_dict(authors_item_data) + + authors.append(authors_item) + + history_version = cls( + id=id, + created_at=created_at, + authors=authors, + ) + + history_version.additional_properties = d + return history_version + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/packages/liveblocks-python/liveblocks/models/yjs_version_authors_item.py b/packages/liveblocks-python/liveblocks/models/history_version_authors_item.py similarity index 88% rename from packages/liveblocks-python/liveblocks/models/yjs_version_authors_item.py rename to packages/liveblocks-python/liveblocks/models/history_version_authors_item.py index 83d7e28c08d..78fff0f50ca 100644 --- a/packages/liveblocks-python/liveblocks/models/yjs_version_authors_item.py +++ b/packages/liveblocks-python/liveblocks/models/history_version_authors_item.py @@ -10,7 +10,7 @@ @_attrs_define -class YjsVersionAuthorsItem: +class HistoryVersionAuthorsItem: """ Attributes: id (str | Unset): User ID of the author @@ -35,12 +35,12 @@ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: d = dict(src_dict) id = d.pop("id", UNSET) - yjs_version_authors_item = cls( + history_version_authors_item = cls( id=id, ) - yjs_version_authors_item.additional_properties = d - return yjs_version_authors_item + history_version_authors_item.additional_properties = d + return history_version_authors_item @property def additional_keys(self) -> list[str]: diff --git a/packages/liveblocks-python/liveblocks/models/yjs_version.py b/packages/liveblocks-python/liveblocks/models/yjs_version.py deleted file mode 100644 index c8450bee7ce..00000000000 --- a/packages/liveblocks-python/liveblocks/models/yjs_version.py +++ /dev/null @@ -1,121 +0,0 @@ -from __future__ import annotations - -import datetime -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Literal, Self, cast - -from attrs import define as _attrs_define -from attrs import field as _attrs_field -from dateutil.parser import isoparse - -from ..types import UNSET, Unset - -if TYPE_CHECKING: - from ..models.yjs_version_authors_item import YjsVersionAuthorsItem - - -@_attrs_define -class YjsVersion: - """ - Example: - {'id': 'vh_abc123', 'type': 'historyVersion', 'createdAt': '2024-10-15T10:30:00.000Z', 'authors': [{'id': - 'user-123'}, {'id': 'user-456'}], 'kind': 'yjs'} - - Attributes: - id (str): Unique identifier for the version - type_ (Literal['historyVersion']): - created_at (datetime.datetime): ISO 8601 timestamp of when the version was created - kind (Literal['yjs']): - authors (list[YjsVersionAuthorsItem] | Unset): List of users who contributed to this version - """ - - id: str - type_: Literal["historyVersion"] - created_at: datetime.datetime - kind: Literal["yjs"] - authors: list[YjsVersionAuthorsItem] | Unset = UNSET - additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) - - def to_dict(self) -> dict[str, Any]: - id = self.id - - type_ = self.type_ - - created_at = self.created_at.isoformat() - - kind = self.kind - - authors: list[dict[str, Any]] | Unset = UNSET - if not isinstance(self.authors, Unset): - authors = [] - for authors_item_data in self.authors: - authors_item = authors_item_data.to_dict() - authors.append(authors_item) - - field_dict: dict[str, Any] = {} - field_dict.update(self.additional_properties) - field_dict.update( - { - "id": id, - "type": type_, - "createdAt": created_at, - "kind": kind, - } - ) - if authors is not UNSET: - field_dict["authors"] = authors - - return field_dict - - @classmethod - def from_dict(cls, src_dict: Mapping[str, Any]) -> Self: - from ..models.yjs_version_authors_item import YjsVersionAuthorsItem - - d = dict(src_dict) - id = d.pop("id") - - type_ = cast(Literal["historyVersion"], d.pop("type")) - if type_ != "historyVersion": - raise ValueError(f"type must match const 'historyVersion', got '{type_}'") - - created_at = isoparse(d.pop("createdAt")) - - kind = cast(Literal["yjs"], d.pop("kind")) - if kind != "yjs": - raise ValueError(f"kind must match const 'yjs', got '{kind}'") - - _authors = d.pop("authors", UNSET) - authors: list[YjsVersionAuthorsItem] | Unset = UNSET - if _authors is not UNSET: - authors = [] - for authors_item_data in _authors: - authors_item = YjsVersionAuthorsItem.from_dict(authors_item_data) - - authors.append(authors_item) - - yjs_version = cls( - id=id, - type_=type_, - created_at=created_at, - kind=kind, - authors=authors, - ) - - yjs_version.additional_properties = d - return yjs_version - - @property - def additional_keys(self) -> list[str]: - return list(self.additional_properties.keys()) - - def __getitem__(self, key: str) -> Any: - return self.additional_properties[key] - - def __setitem__(self, key: str, value: Any) -> None: - self.additional_properties[key] = value - - def __delitem__(self, key: str) -> None: - del self.additional_properties[key] - - def __contains__(self, key: str) -> bool: - return key in self.additional_properties From 15681e140bea1788eebdb59a6277a54938779853 Mon Sep 17 00:00:00 2001 From: Marc Bouchenoire Date: Mon, 29 Jun 2026 13:35:23 +0200 Subject: [PATCH 8/8] Update example READMEs (#3556) --- .github/assets/examples/comments-private.png | Bin 0 -> 223310 bytes examples/nextjs-ai-app-builder/README.md | 2 +- examples/nextjs-ai-dashboard-reports/README.md | 2 +- examples/nextjs-ai-elements-realtime/README.md | 2 +- .../components/HelpButton.tsx | 2 +- examples/nextjs-ai-spreadsheet/README.md | 2 +- .../components/HelpButton.tsx | 3 ++- examples/nextjs-comments-ai/README.md | 2 +- examples/nextjs-comments-audio/README.md | 2 +- examples/nextjs-comments-canvas/README.md | 2 +- .../nextjs-comments-emails-resend/README.md | 2 +- .../nextjs-comments-emails-sendgrid/README.md | 2 +- .../nextjs-comments-notifications/README.md | 2 +- examples/nextjs-comments-overlay/README.md | 2 +- examples/nextjs-comments-primitives/README.md | 2 +- examples/nextjs-comments-private/README.md | 4 ++-- examples/nextjs-comments-search/README.md | 2 +- examples/nextjs-comments-tiptap/README.md | 2 +- examples/nextjs-comments-video/README.md | 2 +- .../nextjs-lexical-emails-resend/README.md | 2 +- examples/nextjs-lexical/README.md | 2 +- .../nextjs-multiplayer-handsontable/README.md | 2 +- examples/nextjs-notifications-custom/README.md | 2 +- examples/nextjs-react-flow-ai/README.md | 2 +- examples/nextjs-tiptap-ai/README.md | 3 --- examples/nextjs-tiptap-emails-resend/README.md | 2 +- .../nextjs-yjs-blocknote-advanced/README.md | 3 --- examples/nextjs-yjs-blocknote/README.md | 3 --- examples/nextjs-yjs-lexical/README.md | 3 --- examples/nextjs-yjs-monaco/README.md | 2 +- examples/nextjs-yjs-tiptap/README.md | 3 --- 31 files changed, 27 insertions(+), 41 deletions(-) create mode 100644 .github/assets/examples/comments-private.png diff --git a/.github/assets/examples/comments-private.png b/.github/assets/examples/comments-private.png new file mode 100644 index 0000000000000000000000000000000000000000..523fcd510c42759398d4d1b3476d6775a9c8a4a9 GIT binary patch literal 223310 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%0Dx!IS=Uj0(0-#I!#6E}tcOpp*i(n#(rJZ5TD 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 m&EJ3e5C8r@|Lwc9_`d){(`bH;HCXfj0000 - + Live Preview diff --git a/examples/nextjs-ai-dashboard-reports/README.md b/examples/nextjs-ai-dashboard-reports/README.md index 0cebb5e6f25..8a9cc235808 100644 --- a/examples/nextjs-ai-dashboard-reports/README.md +++ b/examples/nextjs-ai-dashboard-reports/README.md @@ -10,7 +10,7 @@ # AI chat in reports dashboard

- + Live Preview diff --git a/examples/nextjs-ai-elements-realtime/README.md b/examples/nextjs-ai-elements-realtime/README.md index 4a4a26850c1..b8f34be90f3 100644 --- a/examples/nextjs-ai-elements-realtime/README.md +++ b/examples/nextjs-ai-elements-realtime/README.md @@ -10,7 +10,7 @@ # Realtime AI chat with AI Elements

- + Live Preview diff --git a/examples/nextjs-ai-elements-realtime/components/HelpButton.tsx b/examples/nextjs-ai-elements-realtime/components/HelpButton.tsx index 4e796010568..65dcf39b8e6 100644 --- a/examples/nextjs-ai-elements-realtime/components/HelpButton.tsx +++ b/examples/nextjs-ai-elements-realtime/components/HelpButton.tsx @@ -6,7 +6,7 @@ import { Button } from "./ui/button"; const EXAMPLE_NAME = "Realtime AI chat with AI Elements"; const EXAMPLE_URL = - "https://liveblocks.io/examples/nextjs-ai-elements-realtime"; + "https://liveblocks.io/examples/ai-elements-realtime/nextjs-ai-elements-realtime"; type Feature = { icon: ReactNode; diff --git a/examples/nextjs-ai-spreadsheet/README.md b/examples/nextjs-ai-spreadsheet/README.md index 3a5fd776a52..3cb4c224e98 100644 --- a/examples/nextjs-ai-spreadsheet/README.md +++ b/examples/nextjs-ai-spreadsheet/README.md @@ -10,7 +10,7 @@ # Realtime AI spreadsheet

- + Live Preview diff --git a/examples/nextjs-ai-spreadsheet/components/HelpButton.tsx b/examples/nextjs-ai-spreadsheet/components/HelpButton.tsx index d38de069aaf..242ab8ce9fa 100644 --- a/examples/nextjs-ai-spreadsheet/components/HelpButton.tsx +++ b/examples/nextjs-ai-spreadsheet/components/HelpButton.tsx @@ -5,7 +5,8 @@ import { createPortal } from "react-dom"; import { Button } from "./ui/button"; const EXAMPLE_NAME = "Realtime AI spreadsheet"; -const EXAMPLE_URL = "https://liveblocks.io/examples/nextjs-ai-spreadsheet"; +const EXAMPLE_URL = + "https://liveblocks.io/examples/ai-spreadsheet/nextjs-ai-spreadsheet"; type Feature = { icon: ReactNode; diff --git a/examples/nextjs-comments-ai/README.md b/examples/nextjs-comments-ai/README.md index 69703b581c0..24c032c9d22 100644 --- a/examples/nextjs-comments-ai/README.md +++ b/examples/nextjs-comments-ai/README.md @@ -10,7 +10,7 @@ # Comments with AI replies

- + Live Preview diff --git a/examples/nextjs-comments-audio/README.md b/examples/nextjs-comments-audio/README.md index fc52055f184..fabe594fc62 100644 --- a/examples/nextjs-comments-audio/README.md +++ b/examples/nextjs-comments-audio/README.md @@ -10,7 +10,7 @@ # Audio Comments

- + Live Preview diff --git a/examples/nextjs-comments-canvas/README.md b/examples/nextjs-comments-canvas/README.md index 4c33b750017..b9d945b73f3 100644 --- a/examples/nextjs-comments-canvas/README.md +++ b/examples/nextjs-comments-canvas/README.md @@ -10,7 +10,7 @@ # Canvas Comments

- + Live Preview diff --git a/examples/nextjs-comments-emails-resend/README.md b/examples/nextjs-comments-emails-resend/README.md index a132b4625dc..d52f9c06f38 100644 --- a/examples/nextjs-comments-emails-resend/README.md +++ b/examples/nextjs-comments-emails-resend/README.md @@ -10,7 +10,7 @@ # Comments Notification Emails (Resend)

- + Live Preview diff --git a/examples/nextjs-comments-emails-sendgrid/README.md b/examples/nextjs-comments-emails-sendgrid/README.md index b546848db2b..adfbe95c6c3 100644 --- a/examples/nextjs-comments-emails-sendgrid/README.md +++ b/examples/nextjs-comments-emails-sendgrid/README.md @@ -10,7 +10,7 @@ # Comments Notification Emails (SendGrid)

- + Live Preview diff --git a/examples/nextjs-comments-notifications/README.md b/examples/nextjs-comments-notifications/README.md index 9436433bea5..936428181f7 100644 --- a/examples/nextjs-comments-notifications/README.md +++ b/examples/nextjs-comments-notifications/README.md @@ -10,7 +10,7 @@ # Comments Notifications

- + Live Preview diff --git a/examples/nextjs-comments-overlay/README.md b/examples/nextjs-comments-overlay/README.md index 3d35034143a..8ed39e2689e 100644 --- a/examples/nextjs-comments-overlay/README.md +++ b/examples/nextjs-comments-overlay/README.md @@ -10,7 +10,7 @@ # Overlay Comments

- + Live Preview diff --git a/examples/nextjs-comments-primitives/README.md b/examples/nextjs-comments-primitives/README.md index b8627037b69..b851ecd8760 100644 --- a/examples/nextjs-comments-primitives/README.md +++ b/examples/nextjs-comments-primitives/README.md @@ -10,7 +10,7 @@ # Comments Primitives

- + Live Preview diff --git a/examples/nextjs-comments-private/README.md b/examples/nextjs-comments-private/README.md index a400f3b09bc..5e424b6e82c 100644 --- a/examples/nextjs-comments-private/README.md +++ b/examples/nextjs-comments-private/README.md @@ -10,7 +10,7 @@ # Private Commenting

- + Live Preview @@ -23,7 +23,7 @@ This example shows how to add private commenting to your app with [Liveblocks](https://liveblocks.io) and [Next.js](https://nextjs.org/). -Private Comments +Private Commenting ## Getting started diff --git a/examples/nextjs-comments-search/README.md b/examples/nextjs-comments-search/README.md index 919e36ba8ee..663a241e473 100644 --- a/examples/nextjs-comments-search/README.md +++ b/examples/nextjs-comments-search/README.md @@ -10,7 +10,7 @@ # Comments search

- + Live Preview diff --git a/examples/nextjs-comments-tiptap/README.md b/examples/nextjs-comments-tiptap/README.md index 876dc3c8120..fc76f478106 100644 --- a/examples/nextjs-comments-tiptap/README.md +++ b/examples/nextjs-comments-tiptap/README.md @@ -10,7 +10,7 @@ # Text Editor Comments (Tiptap)

- + Live Preview diff --git a/examples/nextjs-comments-video/README.md b/examples/nextjs-comments-video/README.md index 18c11ca5927..c1bfcd4bd6e 100644 --- a/examples/nextjs-comments-video/README.md +++ b/examples/nextjs-comments-video/README.md @@ -10,7 +10,7 @@ # Video Comments

- + Live Preview diff --git a/examples/nextjs-lexical-emails-resend/README.md b/examples/nextjs-lexical-emails-resend/README.md index 9548559464e..f52a898693b 100644 --- a/examples/nextjs-lexical-emails-resend/README.md +++ b/examples/nextjs-lexical-emails-resend/README.md @@ -10,7 +10,7 @@ # Lexical Notification Emails (Resend)

- + Live Preview diff --git a/examples/nextjs-lexical/README.md b/examples/nextjs-lexical/README.md index f8c882ecc79..e40d03c24c5 100644 --- a/examples/nextjs-lexical/README.md +++ b/examples/nextjs-lexical/README.md @@ -10,7 +10,7 @@ # Collaborative Rich Text Editor (Lexical)

- + Live Preview diff --git a/examples/nextjs-multiplayer-handsontable/README.md b/examples/nextjs-multiplayer-handsontable/README.md index 270965e531f..7a8adc280e9 100644 --- a/examples/nextjs-multiplayer-handsontable/README.md +++ b/examples/nextjs-multiplayer-handsontable/README.md @@ -10,7 +10,7 @@ # Multiplayer Handsontable

- + Live Preview diff --git a/examples/nextjs-notifications-custom/README.md b/examples/nextjs-notifications-custom/README.md index 3fcddd205e9..c578a3f53ee 100644 --- a/examples/nextjs-notifications-custom/README.md +++ b/examples/nextjs-notifications-custom/README.md @@ -10,7 +10,7 @@ # Custom Notifications

- + Live Preview diff --git a/examples/nextjs-react-flow-ai/README.md b/examples/nextjs-react-flow-ai/README.md index 5324e6f9803..55443df35d3 100644 --- a/examples/nextjs-react-flow-ai/README.md +++ b/examples/nextjs-react-flow-ai/README.md @@ -10,7 +10,7 @@ # Collaborative React Flow with AI

- + Live Preview diff --git a/examples/nextjs-tiptap-ai/README.md b/examples/nextjs-tiptap-ai/README.md index a1cdc62013c..1915856152a 100644 --- a/examples/nextjs-tiptap-ai/README.md +++ b/examples/nextjs-tiptap-ai/README.md @@ -10,9 +10,6 @@ # Collaborative Rich Text Editor with AI (Tiptap)

- - Live Preview - Open in CodeSandbox diff --git a/examples/nextjs-tiptap-emails-resend/README.md b/examples/nextjs-tiptap-emails-resend/README.md index fe56549a5ae..909edf5f913 100644 --- a/examples/nextjs-tiptap-emails-resend/README.md +++ b/examples/nextjs-tiptap-emails-resend/README.md @@ -10,7 +10,7 @@ # TipTap Notifications Emails (Resend)

- + Live Preview diff --git a/examples/nextjs-yjs-blocknote-advanced/README.md b/examples/nextjs-yjs-blocknote-advanced/README.md index 4b73d148d74..1adb8120496 100644 --- a/examples/nextjs-yjs-blocknote-advanced/README.md +++ b/examples/nextjs-yjs-blocknote-advanced/README.md @@ -10,9 +10,6 @@ # Advanced Collaborative Rich Text Editor (BlockNote)

- - Live Preview - Open in CodeSandbox diff --git a/examples/nextjs-yjs-blocknote/README.md b/examples/nextjs-yjs-blocknote/README.md index 9d9edc55acd..a066bf6e3a1 100644 --- a/examples/nextjs-yjs-blocknote/README.md +++ b/examples/nextjs-yjs-blocknote/README.md @@ -10,9 +10,6 @@ # Collaborative Rich Text Editor (BlockNote)

- - Live Preview - Open in CodeSandbox diff --git a/examples/nextjs-yjs-lexical/README.md b/examples/nextjs-yjs-lexical/README.md index ca79a3d7bb3..ceabcde6895 100644 --- a/examples/nextjs-yjs-lexical/README.md +++ b/examples/nextjs-yjs-lexical/README.md @@ -10,9 +10,6 @@ # Collaborative Rich Text Editor (Lexical)

- - Live Preview - Open in CodeSandbox diff --git a/examples/nextjs-yjs-monaco/README.md b/examples/nextjs-yjs-monaco/README.md index 20ef9defcd0..7e8cc935aa3 100644 --- a/examples/nextjs-yjs-monaco/README.md +++ b/examples/nextjs-yjs-monaco/README.md @@ -10,7 +10,7 @@ # Collaborative Code Editor (Monaco)

- + Live Preview diff --git a/examples/nextjs-yjs-tiptap/README.md b/examples/nextjs-yjs-tiptap/README.md index 0eaccfd1022..7c089abc95f 100644 --- a/examples/nextjs-yjs-tiptap/README.md +++ b/examples/nextjs-yjs-tiptap/README.md @@ -10,9 +10,6 @@ # Collaborative Rich Text Editor (Tiptap)

- - Live Preview - Open in CodeSandbox