Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/assets/examples/comments-private.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
## 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`

- **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`.

### `@liveblocks/react`

- 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`

- 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`
Expand Down
58 changes: 58 additions & 0 deletions docs/pages/api-reference/liveblocks-client.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2914,6 +2914,11 @@ console.log(inboxNotifications);
<PropertiesListItem name="resolved" type="boolean">
Only return `resolved` or `unresolved` threads. [Learn more](#filtering-resolved-status).
</PropertiesListItem>
<PropertiesListItem name="visibility" type='"public" | "private"'>
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).
</PropertiesListItem>
<PropertiesListItem name="subscribed" type="boolean">
Only return `subscribed` or `unsubscribed` threads. [Learn more](#filtering-subscribed-status).
</PropertiesListItem>
Expand All @@ -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
Expand Down Expand Up @@ -3171,6 +3195,15 @@ const thread = await room.createThread({
Custom metadata to be attached to the thread, see [defining thread
metadata](#defining-thread-metadata).
</PropertiesListItem>
<PropertiesListItem
name="visibility"
type='"public" | "private"'
defaultValue='"public"'
>
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.
</PropertiesListItem>
</PropertiesList>

#### Creating thread content [#creating-thread-content]
Expand Down Expand Up @@ -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).

<Banner>

Private threads are only available on Team and Enterprise plans.

</Banner>

### Room.deleteThread

Deletes a thread by its ID.
Expand Down
37 changes: 23 additions & 14 deletions docs/pages/api-reference/liveblocks-node.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ const { body, status } = await liveblocks.identifyUser({
```

<Banner>
Learn how to
[get started with ID tokens](/docs/authentication#id-token).
Learn how to [get started with ID tokens](/docs/authentication#id-token).
</Banner>

A number of options are also available, enabling you to set up permissions and
Expand Down Expand Up @@ -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({
Expand All @@ -114,7 +114,8 @@ const { body, status } = await liveblocks.identifyUser({
```

<Banner>
Learn more about [ID token permissions](/docs/authentication#id-token-room-permissions).
Learn more about [ID token
permissions](/docs/authentication#id-token-room-permissions).
</Banner>

##### Text editor user data
Expand Down Expand Up @@ -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({
Expand All @@ -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",
Expand All @@ -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.

Expand Down Expand Up @@ -1991,8 +1997,8 @@ You can also convert a Markdown string to a `CommentBody` with

</Banner>

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({
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions docs/pages/api-reference/liveblocks-react-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Composer 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
[`useThreads`](/docs/api-reference/liveblocks-react#useThreads).

<Banner>

Private threads are only available on Team and Enterprise plans.

</Banner>

##### Replying to a thread

If you provide a `threadId`, then submitting the composer will add a new reply
Expand Down Expand Up @@ -1562,6 +1587,14 @@ Learn more about mutation hooks under
<PropertiesListItem name="metadata" type="ThreadMetadata">
The metadata of the thread to create.
</PropertiesListItem>
<PropertiesListItem
name="visibility"
type='"public" | "private"'
defaultValue='"public"'
>
Whether to create a public or private thread. Only applies when creating a
new thread, and requires write access to the selected visibility.
</PropertiesListItem>
<PropertiesListItem
name="commentMetadata"
type="CommentMetadata | Partial<CommentMetadata>"
Expand Down
64 changes: 53 additions & 11 deletions docs/pages/api-reference/liveblocks-react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,8 @@ useErrorListener((error) => {

// 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":
Expand Down Expand Up @@ -4074,8 +4075,9 @@ function Component() {
Optional configuration object.
</PropertiesListItem>
<PropertiesListItem name="option.query" type="ThreadsQuery">
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).
</PropertiesListItem>
<PropertiesListItem name="option.scrollOnLoad" type="boolean">
Whether to scroll to a comment if the URL's hash is set to a comment ID.
Expand Down Expand Up @@ -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, ... }
Expand All @@ -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,

Expand Down Expand Up @@ -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";
Expand All @@ -4291,6 +4300,7 @@ const thread = createThread({
attachments: [],
metadata: {},
commentMetadata: {},
visibility: "private",
});
```

Expand All @@ -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.
</PropertiesListItem>
</PropertiesList>

Expand All @@ -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}`);
}
});
Expand Down Expand Up @@ -6424,6 +6436,36 @@ const { versions, error, isLoading } = useHistoryVersions();
</PropertiesListItem>
</PropertiesList>

### 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);
```

<PropertiesList title="Arguments">
<PropertiesListItem name="versionId" type="string">
The ID of the version to retrieve. Obtained from the `id` field of a
`HistoryVersion` returned by [`useHistoryVersions`][].
</PropertiesListItem>
</PropertiesList>

<PropertiesList title="Returns">
<PropertiesListItem name="data" type="Uint8Array | undefined">
The raw Yjs binary data for the version, or `undefined` while loading.
</PropertiesListItem>
<PropertiesListItem name="isLoading" type="boolean">
Whether the version data is currently being loaded.
</PropertiesListItem>
<PropertiesListItem name="error" type="Error | undefined">
Any error that occurred while loading the version data.
</PropertiesListItem>
</PropertiesList>

## Miscellaneous

### useUser [@badge=Both]
Expand Down
Loading
Loading