, NoInfr>]
>
): {
- room: Room;
+ room: Room
;
leave: () => void;
};
@@ -471,7 +486,7 @@ export type Client<
* will probably happen if you do.
*/
// TODO Make this a getter, so we can provide M
- readonly [kInternal]: PrivateClientApi;
+ readonly [kInternal]: PrivateClientApi;
/**
* Returns the current global sync status of the Liveblocks client. If any
@@ -701,10 +716,12 @@ export function createClient(
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json,
+ FMD extends Json,
>(
details: RoomDetails
): {
- room: Room
;
+ room: Room
;
leave: () => void;
} {
// Create a new self-destructing leave function
@@ -725,7 +742,7 @@ export function createClient(
details.unsubs.add(leave);
return {
- room: details.room as Room
,
+ room: details.room as Room
,
leave,
};
}
@@ -737,6 +754,8 @@ export function createClient(
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json,
+ FMD extends Json,
>(
roomId: string,
...args: OptionalTupleUnless<
@@ -744,7 +763,7 @@ export function createClient(
[options: EnterOptions, NoInfr>]
>
): {
- room: Room;
+ room: Room
;
leave: () => void;
} {
const existing = roomsById.get(roomId);
@@ -763,7 +782,7 @@ export function createClient(
? options.initialStorage(roomId)
: options.initialStorage) ?? ({} as S);
- const newRoom = createRoom
(
+ const newRoom = createRoom
(
{ initialPresence, initialStorage },
{
roomId,
@@ -824,9 +843,11 @@ export function createClient(
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
- >(roomId: string): Room
| null {
+ FM extends Json,
+ FMD extends Json,
+ >(roomId: string): Room
| null {
const room = roomsById.get(roomId)?.room;
- return room ? (room as Room
) : null;
+ return room ? (room as Room
) : null;
}
function logout() {
diff --git a/packages/liveblocks-core/src/globals/augmentation.ts b/packages/liveblocks-core/src/globals/augmentation.ts
index fa630ea8db8..440383696af 100644
--- a/packages/liveblocks-core/src/globals/augmentation.ts
+++ b/packages/liveblocks-core/src/globals/augmentation.ts
@@ -24,6 +24,8 @@ type ExtendableTypes =
| "RoomEvent"
| "ThreadMetadata"
| "CommentMetadata"
+ | "FeedMetadata"
+ | "FeedMessageData"
| "RoomInfo"
| "GroupInfo"
| "ActivitiesData";
@@ -80,6 +82,10 @@ export type DTM = GetOverride<"ThreadMetadata", BaseMetadata>;
export type DCM = GetOverride<"CommentMetadata", BaseMetadata>;
+export type DFM = GetOverride<"FeedMetadata", Json, "is not a valid JSON value">;
+
+export type DFMD = GetOverride<"FeedMessageData", Json, "is not a valid JSON value">;
+
export type DRI = GetOverride<"RoomInfo", BaseRoomInfo>;
export type DGI = GetOverride<"GroupInfo", BaseGroupInfo>;
diff --git a/packages/liveblocks-core/src/index.ts b/packages/liveblocks-core/src/index.ts
index d4ec50e0c0f..eb48f92481e 100644
--- a/packages/liveblocks-core/src/index.ts
+++ b/packages/liveblocks-core/src/index.ts
@@ -99,6 +99,8 @@ export type {
DAD,
DCM,
DE,
+ DFM,
+ DFMD,
DGI,
DP,
DRI,
@@ -202,6 +204,9 @@ export type { BaseUserMeta, IUserInfo } from "./protocol/BaseUserMeta";
export type {
BroadcastEventClientMsg,
ClientMsg,
+ FeedCreateMetadata,
+ FeedFetchMetadataFilter,
+ FeedUpdateMetadata,
FetchStorageClientMsg,
FetchYDocClientMsg,
UpdatePresenceClientMsg,
@@ -240,6 +245,7 @@ export type {
ThreadDataWithDeleteInfo,
} from "./protocol/Comments";
export type { ThreadDeleteInfo } from "./protocol/Comments";
+export type { Feed, FeedMessage } from "./protocol/Feeds";
export type {
GroupData,
GroupDataPlain,
@@ -295,6 +301,17 @@ export type {
export type {
BroadcastedEventServerMsg,
CommentsEventServerMsg,
+ FeedDeletedServerMsg,
+ FeedMessagesAddedServerMsg,
+ FeedMessagesDeletedServerMsg,
+ FeedMessagesListServerMsg,
+ FeedMessagesUpdatedServerMsg,
+ FeedRequestError,
+ FeedRequestFailedServerMsg,
+ FeedsAddedServerMsg,
+ FeedsEventServerMsg,
+ FeedsListServerMsg,
+ FeedsUpdatedServerMsg,
RejectedStorageOpServerMsg,
RoomStateServerMsg,
ServerMsg,
@@ -305,7 +322,7 @@ export type {
UserLeftServerMsg,
YDocUpdateServerMsg,
} from "./protocol/ServerMsg";
-export { ServerMsgCode } from "./protocol/ServerMsg";
+export { FeedRequestErrorCode, ServerMsgCode } from "./protocol/ServerMsg";
export type {
ChildStorageNode,
CompactChildNode,
diff --git a/packages/liveblocks-core/src/protocol/AuthToken.ts b/packages/liveblocks-core/src/protocol/AuthToken.ts
index 735d92abcd2..ebb0c8da445 100644
--- a/packages/liveblocks-core/src/protocol/AuthToken.ts
+++ b/packages/liveblocks-core/src/protocol/AuthToken.ts
@@ -9,6 +9,7 @@ export enum Permission {
PresenceWrite = "room:presence:write",
CommentsWrite = "comments:write",
CommentsRead = "comments:read",
+ FeedsWrite = "feeds:write",
}
export type LiveblocksPermissions = Record;
diff --git a/packages/liveblocks-core/src/protocol/ClientMsg.ts b/packages/liveblocks-core/src/protocol/ClientMsg.ts
index d61a568149c..62865ff6519 100644
--- a/packages/liveblocks-core/src/protocol/ClientMsg.ts
+++ b/packages/liveblocks-core/src/protocol/ClientMsg.ts
@@ -14,6 +14,16 @@ export const ClientMsgCode = Object.freeze({
// For Yjs support
FETCH_YDOC: 300,
UPDATE_YDOC: 301,
+
+ // For Feeds
+ FETCH_FEEDS: 510,
+ FETCH_FEED_MESSAGES: 511,
+ ADD_FEED: 512,
+ UPDATE_FEED: 513,
+ DELETE_FEED: 514,
+ ADD_FEED_MESSAGE: 515,
+ UPDATE_FEED_MESSAGE: 516,
+ DELETE_FEED_MESSAGE: 517,
});
export namespace ClientMsgCode {
@@ -23,6 +33,14 @@ export namespace ClientMsgCode {
export type UPDATE_STORAGE = typeof ClientMsgCode.UPDATE_STORAGE;
export type FETCH_YDOC = typeof ClientMsgCode.FETCH_YDOC;
export type UPDATE_YDOC = typeof ClientMsgCode.UPDATE_YDOC;
+ export type FETCH_FEEDS = typeof ClientMsgCode.FETCH_FEEDS;
+ export type FETCH_FEED_MESSAGES = typeof ClientMsgCode.FETCH_FEED_MESSAGES;
+ export type ADD_FEED = typeof ClientMsgCode.ADD_FEED;
+ export type UPDATE_FEED = typeof ClientMsgCode.UPDATE_FEED;
+ export type DELETE_FEED = typeof ClientMsgCode.DELETE_FEED;
+ export type ADD_FEED_MESSAGE = typeof ClientMsgCode.ADD_FEED_MESSAGE;
+ export type UPDATE_FEED_MESSAGE = typeof ClientMsgCode.UPDATE_FEED_MESSAGE;
+ export type DELETE_FEED_MESSAGE = typeof ClientMsgCode.DELETE_FEED_MESSAGE;
}
/**
@@ -39,7 +57,17 @@ export type ClientMsg =
// For Yjs support
| FetchYDocClientMsg
- | UpdateYDocClientMsg;
+ | UpdateYDocClientMsg
+
+ // For Feeds
+ | FetchFeedsClientMsg
+ | FetchFeedMessagesClientMsg
+ | AddFeedClientMsg
+ | UpdateFeedClientMsg
+ | DeleteFeedClientMsg
+ | AddFeedMessageClientMsg
+ | UpdateFeedMessageClientMsg
+ | DeleteFeedMessageClientMsg;
export type BroadcastEventClientMsg = {
type: ClientMsgCode.BROADCAST_EVENT;
@@ -104,3 +132,76 @@ export type UpdateYDocClientMsg = {
readonly guid?: string; // an optional guid to identify a subdoc
readonly v2?: boolean; // if it's a v2 update
};
+
+/** Metadata filter for {@link FetchFeedsClientMsg}. Values are matched as strings. */
+export type FeedFetchMetadataFilter = Record;
+
+/** Metadata for {@link AddFeedClientMsg}. */
+export type FeedCreateMetadata = Record;
+
+/** Metadata for {@link UpdateFeedClientMsg}. Use `null` to remove a key. */
+export type FeedUpdateMetadata = Record;
+
+export type FetchFeedsClientMsg = {
+ readonly type: ClientMsgCode.FETCH_FEEDS;
+ readonly requestId: string;
+ readonly cursor?: string;
+ readonly since?: number;
+ readonly limit?: number;
+ readonly metadata?: FeedFetchMetadataFilter;
+};
+
+export type FetchFeedMessagesClientMsg = {
+ readonly type: ClientMsgCode.FETCH_FEED_MESSAGES;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly cursor?: string;
+ readonly since?: number;
+ readonly limit?: number;
+};
+
+export type AddFeedClientMsg = {
+ readonly type: ClientMsgCode.ADD_FEED;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly metadata?: FeedCreateMetadata;
+ readonly createdAt?: number;
+};
+
+export type UpdateFeedClientMsg = {
+ readonly type: ClientMsgCode.UPDATE_FEED;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly metadata: FeedUpdateMetadata;
+};
+
+export type DeleteFeedClientMsg = {
+ readonly type: ClientMsgCode.DELETE_FEED;
+ readonly requestId: string;
+ readonly feedId: string;
+};
+
+export type AddFeedMessageClientMsg = {
+ readonly type: ClientMsgCode.ADD_FEED_MESSAGE;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly data: JsonObject;
+ readonly id?: string;
+ readonly createdAt?: number;
+};
+
+export type UpdateFeedMessageClientMsg = {
+ readonly type: ClientMsgCode.UPDATE_FEED_MESSAGE;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly messageId: string;
+ readonly data: JsonObject;
+ readonly updatedAt?: number;
+};
+
+export type DeleteFeedMessageClientMsg = {
+ readonly type: ClientMsgCode.DELETE_FEED_MESSAGE;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly messageId: string;
+};
diff --git a/packages/liveblocks-core/src/protocol/Feeds.ts b/packages/liveblocks-core/src/protocol/Feeds.ts
new file mode 100644
index 00000000000..d5a82e766b4
--- /dev/null
+++ b/packages/liveblocks-core/src/protocol/Feeds.ts
@@ -0,0 +1,15 @@
+import type { Json } from "../lib/Json";
+
+export type Feed = {
+ feedId: string;
+ metadata: FM;
+ createdAt: number;
+ updatedAt: number;
+};
+
+export type FeedMessage = {
+ id: string;
+ createdAt: number;
+ updatedAt: number;
+ data: FMD;
+};
diff --git a/packages/liveblocks-core/src/protocol/ServerMsg.ts b/packages/liveblocks-core/src/protocol/ServerMsg.ts
index 2ef037970da..75ca89bbb84 100644
--- a/packages/liveblocks-core/src/protocol/ServerMsg.ts
+++ b/packages/liveblocks-core/src/protocol/ServerMsg.ts
@@ -1,5 +1,6 @@
import type { Json, JsonObject } from "../lib/Json";
import type { BaseUserMeta } from "./BaseUserMeta";
+import type { Feed, FeedMessage } from "./Feeds";
import type { ServerWireOp } from "./Op";
import type { CompactNode, StorageNode } from "./StorageNode";
@@ -33,6 +34,17 @@ export const ServerMsgCode = Object.freeze({
COMMENT_REACTION_REMOVED: 406,
COMMENT_METADATA_UPDATED: 409,
+ // For Feeds
+ FEEDS_LIST: 500,
+ FEEDS_ADDED: 501,
+ FEEDS_UPDATED: 502,
+ FEED_DELETED: 503,
+ FEED_MESSAGES_LIST: 504,
+ FEED_MESSAGES_ADDED: 505,
+ FEED_MESSAGES_UPDATED: 506,
+ FEED_MESSAGES_DELETED: 507,
+ FEED_REQUEST_FAILED: 508,
+
// Error codes
REJECT_STORAGE_OP: 299, // Sent if a mutation was not allowed on the server (i.e. due to permissions, limit exceeded, etc)
});
@@ -60,6 +72,17 @@ export namespace ServerMsgCode {
typeof ServerMsgCode.COMMENT_REACTION_ADDED;
export type COMMENT_REACTION_REMOVED =
typeof ServerMsgCode.COMMENT_REACTION_REMOVED;
+ export type FEEDS_LIST = typeof ServerMsgCode.FEEDS_LIST;
+ export type FEEDS_ADDED = typeof ServerMsgCode.FEEDS_ADDED;
+ export type FEEDS_UPDATED = typeof ServerMsgCode.FEEDS_UPDATED;
+ export type FEED_DELETED = typeof ServerMsgCode.FEED_DELETED;
+ export type FEED_MESSAGES_LIST = typeof ServerMsgCode.FEED_MESSAGES_LIST;
+ export type FEED_MESSAGES_ADDED = typeof ServerMsgCode.FEED_MESSAGES_ADDED;
+ export type FEED_MESSAGES_UPDATED =
+ typeof ServerMsgCode.FEED_MESSAGES_UPDATED;
+ export type FEED_MESSAGES_DELETED =
+ typeof ServerMsgCode.FEED_MESSAGES_DELETED;
+ export type FEED_REQUEST_FAILED = typeof ServerMsgCode.FEED_REQUEST_FAILED;
export type COMMENT_METADATA_UPDATED =
typeof ServerMsgCode.COMMENT_METADATA_UPDATED;
export type REJECT_STORAGE_OP = typeof ServerMsgCode.REJECT_STORAGE_OP;
@@ -89,7 +112,10 @@ export type ServerMsg<
| RejectedStorageOpServerMsg // For a single client
// Comments
- | CommentsEventServerMsg;
+ | CommentsEventServerMsg
+
+ // Feeds
+ | FeedsEventServerMsg;
export type CommentsEventServerMsg =
| ThreadCreatedEvent
@@ -359,3 +385,88 @@ export type RejectedStorageOpServerMsg = {
readonly opIds: string[];
readonly reason: string;
};
+
+export type FeedsEventServerMsg<
+ FM extends Json = Json,
+ FMD extends Json = Json,
+> =
+ | FeedsListServerMsg
+ | FeedsAddedServerMsg
+ | FeedsUpdatedServerMsg
+ | FeedDeletedServerMsg
+ | FeedMessagesListServerMsg
+ | FeedMessagesAddedServerMsg
+ | FeedMessagesUpdatedServerMsg
+ | FeedMessagesDeletedServerMsg
+ | FeedRequestFailedServerMsg;
+
+/** Error codes for {@link FeedRequestFailedServerMsg}. */
+export const FeedRequestErrorCode = {
+ INTERNAL: "INTERNAL",
+ FEED_ALREADY_EXISTS: "FEED_ALREADY_EXISTS",
+ FEED_NOT_FOUND: "FEED_NOT_FOUND",
+ FEED_MESSAGE_NOT_FOUND: "FEED_MESSAGE_NOT_FOUND",
+} as const;
+
+/** String literals accepted in {@link FeedRequestFailedServerMsg}.code */
+export type FeedRequestError =
+ (typeof FeedRequestErrorCode)[keyof typeof FeedRequestErrorCode];
+
+/**
+ * Sent to the client when a feed mutation referenced by `requestId` failed
+ * (e.g. validation or permission error).
+ */
+export type FeedRequestFailedServerMsg = {
+ readonly type: ServerMsgCode.FEED_REQUEST_FAILED;
+ readonly requestId: string;
+ readonly code: string;
+ readonly reason?: string;
+};
+
+export type FeedsListServerMsg = {
+ readonly type: ServerMsgCode.FEEDS_LIST;
+ readonly requestId: string;
+ readonly feeds: Feed[];
+ readonly nextCursor?: string;
+};
+
+export type FeedsAddedServerMsg = {
+ readonly type: ServerMsgCode.FEEDS_ADDED;
+ readonly feeds: Feed[];
+};
+
+export type FeedsUpdatedServerMsg = {
+ readonly type: ServerMsgCode.FEEDS_UPDATED;
+ readonly feeds: Feed[];
+};
+
+export type FeedDeletedServerMsg = {
+ readonly type: ServerMsgCode.FEED_DELETED;
+ readonly feedId: string;
+};
+
+export type FeedMessagesListServerMsg = {
+ readonly type: ServerMsgCode.FEED_MESSAGES_LIST;
+ readonly requestId: string;
+ readonly feedId: string;
+ readonly messages: FeedMessage[];
+ readonly nextCursor?: string;
+};
+
+export type FeedMessagesAddedServerMsg = {
+ readonly type: ServerMsgCode.FEED_MESSAGES_ADDED;
+ readonly feedId: string;
+ readonly messages: FeedMessage[];
+};
+
+export type FeedMessagesUpdatedServerMsg = {
+ readonly type: ServerMsgCode.FEED_MESSAGES_UPDATED;
+ readonly feedId: string;
+ readonly messages: FeedMessage[];
+};
+
+export type FeedMessagesDeletedServerMsg = {
+ readonly type: ServerMsgCode.FEED_MESSAGES_DELETED;
+ readonly feedId: string;
+ readonly messageIds: readonly string[];
+};
diff --git a/packages/liveblocks-core/src/room.ts b/packages/liveblocks-core/src/room.ts
index f8b1a249d73..c35f29c73be 100644
--- a/packages/liveblocks-core/src/room.ts
+++ b/packages/liveblocks-core/src/room.ts
@@ -17,7 +17,16 @@ import {
import { LiveObject } from "./crdts/LiveObject";
import type { LiveStructure, LsonObject } from "./crdts/Lson";
import type { StorageCallback, StorageUpdate } from "./crdts/StorageUpdates";
-import type { DCM, DE, DP, DS, DTM, DU } from "./globals/augmentation";
+import type {
+ DCM,
+ DE,
+ DFM,
+ DFMD,
+ DP,
+ DS,
+ DTM,
+ DU,
+} from "./globals/augmentation";
import { kInternal } from "./internal";
import { assertNever, nn } from "./lib/assert";
import type { BatchStore } from "./lib/batch";
@@ -29,6 +38,7 @@ import { makeEventSource } from "./lib/EventSource";
import * as console from "./lib/fancy-console";
import type { Json, JsonObject } from "./lib/Json";
import { isJsonArray, isJsonObject } from "./lib/Json";
+import { nanoid } from "./lib/nanoid";
import { asPos } from "./lib/position";
import { DerivedSignal, PatchableSignal, Signal } from "./lib/signals";
import { makeStopWatch } from "./lib/stopwatch";
@@ -47,7 +57,21 @@ import type {
import type { Permission } from "./protocol/AuthToken";
import { canComment, canWriteStorage } from "./protocol/AuthToken";
import type { BaseUserMeta, IUserInfo } from "./protocol/BaseUserMeta";
-import type { ClientMsg, UpdateYDocClientMsg } from "./protocol/ClientMsg";
+import type {
+ AddFeedClientMsg,
+ AddFeedMessageClientMsg,
+ ClientMsg,
+ DeleteFeedClientMsg,
+ DeleteFeedMessageClientMsg,
+ FeedCreateMetadata,
+ FeedFetchMetadataFilter,
+ FeedUpdateMetadata,
+ FetchFeedMessagesClientMsg,
+ FetchFeedsClientMsg,
+ UpdateFeedClientMsg,
+ UpdateFeedMessageClientMsg,
+ UpdateYDocClientMsg,
+} from "./protocol/ClientMsg";
import { ClientMsgCode } from "./protocol/ClientMsg";
import type {
BaseMetadata,
@@ -60,6 +84,7 @@ import type {
ThreadData,
ThreadDeleteInfo,
} from "./protocol/Comments";
+import type { Feed, FeedMessage } from "./protocol/Feeds";
import type {
InboxNotificationData,
InboxNotificationDeleteInfo,
@@ -70,6 +95,14 @@ import { isIgnoredOp, OpCode } from "./protocol/Op";
import type { RoomSubscriptionSettings } from "./protocol/RoomSubscriptionSettings";
import type {
CommentsEventServerMsg,
+ FeedMessagesAddedServerMsg,
+ FeedMessagesListServerMsg,
+ FeedMessagesUpdatedServerMsg,
+ FeedRequestFailedServerMsg,
+ FeedsAddedServerMsg,
+ FeedsEventServerMsg,
+ FeedsListServerMsg,
+ FeedsUpdatedServerMsg,
RoomStateServerMsg,
ServerMsg,
UpdatePresenceServerMsg,
@@ -110,6 +143,8 @@ import { PKG_VERSION } from "./version";
export type TimeoutID = ReturnType;
+const FEEDS_TIMEOUT = 5_000; // 5 seconds
+
//
// NOTE:
// This type looks an awful lot like InternalOthersEvent, but don't change this
@@ -506,7 +541,10 @@ export type OpaqueRoom = Room<
LsonObject,
BaseUserMeta,
Json,
- BaseMetadata
+ BaseMetadata,
+ BaseMetadata,
+ Json,
+ Json
>;
export type Room<
@@ -516,6 +554,8 @@ export type Room<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
+ FM extends Json = DFM,
+ FMD extends Json = DFMD,
> = {
/**
* @private
@@ -602,6 +642,84 @@ export type Room<
*/
fetchYDoc(stateVector: string, guid?: string, isV2?: boolean): void;
+ /**
+ * Fetches feeds for the room.
+ */
+ fetchFeeds(options?: {
+ cursor?: string;
+ since?: number;
+ limit?: number;
+ metadata?: FeedFetchMetadataFilter;
+ }): Promise<{
+ feeds: Feed[];
+ nextCursor?: string;
+ }>;
+
+ /**
+ * Fetches messages for a specific feed.
+ */
+ fetchFeedMessages(
+ feedId: string,
+ options?: {
+ cursor?: string;
+ since?: number;
+ limit?: number;
+ }
+ ): Promise<{
+ messages: FeedMessage[];
+ nextCursor?: string;
+ }>;
+
+ /**
+ * Adds a new feed to the room via WebSocket.
+ * Resolves when the server broadcasts the new feed, or rejects on
+ * FEED_REQUEST_FAILED (508) or timeout.
+ */
+ addFeed(
+ feedId: string,
+ options?: {
+ metadata?: FeedCreateMetadata;
+ createdAt?: number;
+ }
+ ): Promise;
+
+ /**
+ * Updates metadata for an existing feed via WebSocket.
+ */
+ updateFeed(feedId: string, metadata: FeedUpdateMetadata): Promise;
+
+ /**
+ * Deletes a feed via WebSocket.
+ */
+ deleteFeed(feedId: string): Promise;
+
+ /**
+ * Adds a new message to a feed via WebSocket.
+ */
+ addFeedMessage(
+ feedId: string,
+ data: JsonObject,
+ options?: {
+ id?: string;
+ createdAt?: number;
+ }
+ ): Promise;
+
+ /**
+ * Updates an existing feed message via WebSocket.
+ */
+ updateFeedMessage(
+ feedId: string,
+ messageId: string,
+ data: JsonObject,
+ options?: { updatedAt?: number }
+ ): Promise;
+
+ /**
+ * Deletes a feed message via WebSocket.
+ */
+ deleteFeedMessage(feedId: string, messageId: string): Promise;
+
/**
* Broadcasts an event to other users in the room. Event broadcasted to the room can be listened with {@link Room.subscribe}("event").
* @param {any} event the event to broadcast. Should be serializable to JSON
@@ -666,6 +784,7 @@ export type Room<
readonly storageStatus: Observable;
readonly ydoc: Observable;
readonly comments: Observable;
+ readonly feeds: Observable>;
/**
* Called right before the room is destroyed. The event cannot be used to
@@ -1375,10 +1494,12 @@ export function createRoom<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = DFM,
+ FMD extends Json = DFMD,
>(
options: { initialPresence: P; initialStorage: S },
config: RoomConfig
-): Room {
+): Room
{
const roomId = config.roomId;
const initialPresence = options.initialPresence; // ?? {};
const initialStorage = options.initialStorage; // ?? {};
@@ -1622,6 +1743,7 @@ export function createRoom<
ydoc: makeEventSource(),
comments: makeEventSource(),
+ feeds: makeEventSource>(),
roomWillDestroy: makeEventSource(),
};
@@ -2199,6 +2321,12 @@ export function createRoom<
sendMessages(messages);
}
+ function isFeedRequestFailedMsg(
+ msg: ServerMsg
+ ): msg is FeedRequestFailedServerMsg {
+ return msg.type === ServerMsgCode.FEED_REQUEST_FAILED;
+ }
+
/**
* Handles a message received on the WebSocket. Will never be a "pong". The
* "pong" is handled at the connection manager level.
@@ -2338,6 +2466,104 @@ export function createRoom<
break;
}
+ case ServerMsgCode.FEEDS_LIST: {
+ const feedsListMsg = message as FeedsListServerMsg;
+ const pending = pendingFeedsRequests.get(feedsListMsg.requestId);
+ if (pending) {
+ pending.resolve({
+ feeds: feedsListMsg.feeds,
+ nextCursor: feedsListMsg.nextCursor,
+ });
+ pendingFeedsRequests.delete(feedsListMsg.requestId);
+ }
+ eventHub.feeds.notify(feedsListMsg);
+ break;
+ }
+
+ case ServerMsgCode.FEEDS_ADDED: {
+ const feedsAddedMsg = message as FeedsAddedServerMsg;
+ eventHub.feeds.notify(feedsAddedMsg);
+ tryResolvePendingFeedMutationsFromFeedsEvent(feedsAddedMsg);
+ break;
+ }
+
+ case ServerMsgCode.FEEDS_UPDATED: {
+ const feedsUpdatedMsg = message as FeedsUpdatedServerMsg;
+ eventHub.feeds.notify(feedsUpdatedMsg);
+ tryResolvePendingFeedMutationsFromFeedsEvent(feedsUpdatedMsg);
+ break;
+ }
+
+ case ServerMsgCode.FEED_DELETED: {
+ eventHub.feeds.notify(message);
+ tryResolvePendingFeedMutationsFromFeedsEvent(message);
+ break;
+ }
+
+ case ServerMsgCode.FEED_MESSAGES_LIST: {
+ const feedMsgsListMsg = message as FeedMessagesListServerMsg;
+ const pending = pendingFeedMessagesRequests.get(
+ feedMsgsListMsg.requestId
+ );
+ if (pending) {
+ pending.resolve({
+ messages: feedMsgsListMsg.messages,
+ nextCursor: feedMsgsListMsg.nextCursor,
+ });
+ pendingFeedMessagesRequests.delete(feedMsgsListMsg.requestId);
+ }
+ eventHub.feeds.notify(feedMsgsListMsg);
+ break;
+ }
+
+ case ServerMsgCode.FEED_MESSAGES_ADDED: {
+ const feedMsgsAddedMsg = message as FeedMessagesAddedServerMsg;
+ eventHub.feeds.notify(feedMsgsAddedMsg);
+ tryResolvePendingFeedMutationsFromFeedsEvent(feedMsgsAddedMsg);
+ break;
+ }
+
+ case ServerMsgCode.FEED_MESSAGES_UPDATED: {
+ const feedMsgsUpdatedMsg =
+ message as FeedMessagesUpdatedServerMsg;
+ eventHub.feeds.notify(feedMsgsUpdatedMsg);
+ tryResolvePendingFeedMutationsFromFeedsEvent(feedMsgsUpdatedMsg);
+ break;
+ }
+
+ case ServerMsgCode.FEED_MESSAGES_DELETED: {
+ eventHub.feeds.notify(message);
+ tryResolvePendingFeedMutationsFromFeedsEvent(message);
+ break;
+ }
+
+ case ServerMsgCode.FEED_REQUEST_FAILED: {
+ if (!isFeedRequestFailedMsg(message)) {
+ break;
+ }
+ const { requestId, code, reason } = message;
+ const err = new LiveblocksError(reason ?? "Feed request failed", {
+ type: "FEED_REQUEST_ERROR",
+ roomId,
+ requestId,
+ code,
+ reason,
+ });
+ if (pendingFeedMutations.has(requestId)) {
+ settleFeedMutation(requestId, "error", err);
+ } else if (pendingFeedsRequests.has(requestId)) {
+ const pending = pendingFeedsRequests.get(requestId);
+ pendingFeedsRequests.delete(requestId);
+ pending?.reject(err);
+ } else if (pendingFeedMessagesRequests.has(requestId)) {
+ const pending = pendingFeedMessagesRequests.get(requestId);
+ pendingFeedMessagesRequests.delete(requestId);
+ pending?.reject(err);
+ }
+ eventHub.feeds.notify(message);
+ break;
+ }
+
case ServerMsgCode.STORAGE_STATE_V7: // No longer used in V8
default:
// Ignore unknown server messages
@@ -2472,6 +2698,220 @@ export function createRoom<
let _getStorage$: Promise | null = null;
let _resolveStoragePromise: (() => void) | null = null;
+ // Pending feeds fetch requests (keyed by requestId)
+ const pendingFeedsRequests = new Map<
+ string,
+ {
+ resolve: (value: { feeds: Feed[]; nextCursor?: string }) => void;
+ reject: (error: Error) => void;
+ }
+ >();
+
+ // Pending feed messages fetch requests (keyed by requestId)
+ const pendingFeedMessagesRequests = new Map<
+ string,
+ {
+ resolve: (value: {
+ messages: FeedMessage[];
+ nextCursor?: string;
+ }) => void;
+ reject: (error: Error) => void;
+ }
+ >();
+
+ type PendingFeedMutationKind =
+ | "add-feed"
+ | "update-feed"
+ | "delete-feed"
+ | "add-message"
+ | "update-message"
+ | "delete-message";
+
+ type PendingFeedMutation = {
+ resolve: () => void;
+ reject: (error: Error) => void;
+ timeoutId: TimeoutID;
+ kind: PendingFeedMutationKind;
+ feedId: string;
+ messageId?: string;
+ expectedClientMessageId?: string;
+ };
+
+ const pendingFeedMutations = new Map();
+ const pendingAddMessageFifoByFeed = new Map();
+
+ function settleFeedMutation(
+ requestId: string,
+ outcome: "ok" | "error",
+ error?: Error
+ ): void {
+ const pending = pendingFeedMutations.get(requestId);
+ if (pending === undefined) {
+ return;
+ }
+ clearTimeout(pending.timeoutId);
+ pendingFeedMutations.delete(requestId);
+ if (pending.kind === "add-message" && !pending.expectedClientMessageId) {
+ const q = pendingAddMessageFifoByFeed.get(pending.feedId);
+ if (q !== undefined) {
+ const idx = q.indexOf(requestId);
+ if (idx >= 0) {
+ q.splice(idx, 1);
+ }
+ if (q.length === 0) {
+ pendingAddMessageFifoByFeed.delete(pending.feedId);
+ }
+ }
+ }
+ if (outcome === "ok") {
+ pending.resolve();
+ } else {
+ pending.reject(error ?? new Error("Feed mutation failed"));
+ }
+ }
+
+ function registerFeedMutation(
+ requestId: string,
+ kind: PendingFeedMutationKind,
+ feedId: string,
+ options?: { messageId?: string; expectedClientMessageId?: string }
+ ): Promise {
+ const { promise, resolve, reject } = Promise_withResolvers();
+ const timeoutId: TimeoutID = setTimeout(() => {
+ if (pendingFeedMutations.has(requestId)) {
+ settleFeedMutation(
+ requestId,
+ "error",
+ new Error("Feed mutation timeout")
+ );
+ }
+ }, FEEDS_TIMEOUT);
+
+ pendingFeedMutations.set(requestId, {
+ resolve,
+ reject,
+ timeoutId,
+ kind,
+ feedId,
+ messageId: options?.messageId,
+ expectedClientMessageId: options?.expectedClientMessageId,
+ });
+
+ if (
+ kind === "add-message" &&
+ options?.expectedClientMessageId === undefined
+ ) {
+ const q = pendingAddMessageFifoByFeed.get(feedId) ?? [];
+ q.push(requestId);
+ pendingAddMessageFifoByFeed.set(feedId, q);
+ }
+
+ return promise;
+ }
+
+ function tryResolvePendingFeedMutationsFromFeedsEvent(
+ message: FeedsEventServerMsg
+ ): void {
+ switch (message.type) {
+ case ServerMsgCode.FEEDS_ADDED: {
+ for (const feed of message.feeds) {
+ for (const [requestId, pending] of [...pendingFeedMutations]) {
+ if (pending.kind === "add-feed" && pending.feedId === feed.feedId) {
+ settleFeedMutation(requestId, "ok");
+ break;
+ }
+ }
+ }
+ break;
+ }
+ case ServerMsgCode.FEEDS_UPDATED: {
+ for (const feed of message.feeds) {
+ for (const [requestId, pending] of [...pendingFeedMutations]) {
+ if (
+ pending.kind === "update-feed" &&
+ pending.feedId === feed.feedId
+ ) {
+ settleFeedMutation(requestId, "ok");
+ }
+ }
+ }
+ break;
+ }
+ case ServerMsgCode.FEED_DELETED: {
+ for (const [requestId, pending] of [...pendingFeedMutations]) {
+ if (
+ pending.kind === "delete-feed" &&
+ pending.feedId === message.feedId
+ ) {
+ settleFeedMutation(requestId, "ok");
+ break;
+ }
+ }
+ break;
+ }
+ case ServerMsgCode.FEED_MESSAGES_ADDED: {
+ for (const m of message.messages) {
+ let matched = false;
+ for (const [requestId, pending] of [...pendingFeedMutations]) {
+ if (
+ pending.kind === "add-message" &&
+ pending.feedId === message.feedId &&
+ pending.expectedClientMessageId === m.id
+ ) {
+ settleFeedMutation(requestId, "ok");
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ const q = pendingAddMessageFifoByFeed.get(message.feedId);
+ const headId = q?.[0];
+ if (headId !== undefined) {
+ const pending = pendingFeedMutations.get(headId);
+ if (
+ pending?.kind === "add-message" &&
+ pending.expectedClientMessageId === undefined
+ ) {
+ settleFeedMutation(headId, "ok");
+ }
+ }
+ }
+ }
+ break;
+ }
+ case ServerMsgCode.FEED_MESSAGES_UPDATED: {
+ for (const m of message.messages) {
+ for (const [requestId, pending] of [...pendingFeedMutations]) {
+ if (
+ pending.kind === "update-message" &&
+ pending.feedId === message.feedId &&
+ pending.messageId === m.id
+ ) {
+ settleFeedMutation(requestId, "ok");
+ }
+ }
+ }
+ break;
+ }
+ case ServerMsgCode.FEED_MESSAGES_DELETED: {
+ for (const mid of message.messageIds) {
+ for (const [requestId, pending] of [...pendingFeedMutations]) {
+ if (
+ pending.kind === "delete-message" &&
+ pending.feedId === message.feedId &&
+ pending.messageId === mid
+ ) {
+ settleFeedMutation(requestId, "ok");
+ }
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
function processInitialStorage(nodes: NodeMap) {
const unacknowledgedOps = new Map(context.unacknowledgedOps);
createOrUpdateRootFromMessage(nodes);
@@ -2582,6 +3022,191 @@ export function createRoom<
flushNowOrSoon();
}
+ async function fetchFeeds(options?: {
+ cursor?: string;
+ since?: number;
+ limit?: number;
+ metadata?: FeedFetchMetadataFilter;
+ }): Promise<{ feeds: Feed[]; nextCursor?: string }> {
+ const requestId = nanoid();
+
+ const { promise, resolve, reject } = Promise_withResolvers<{
+ feeds: Feed[];
+ nextCursor?: string;
+ }>();
+
+ pendingFeedsRequests.set(requestId, { resolve, reject });
+
+ const message: FetchFeedsClientMsg = {
+ type: ClientMsgCode.FETCH_FEEDS,
+ requestId,
+ cursor: options?.cursor,
+ since: options?.since,
+ limit: options?.limit,
+ metadata: options?.metadata,
+ };
+
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+
+ setTimeout(() => {
+ if (pendingFeedsRequests.has(requestId)) {
+ pendingFeedsRequests.delete(requestId);
+ reject(new Error("Feeds fetch timeout"));
+ }
+ }, FEEDS_TIMEOUT);
+
+ return promise;
+ }
+
+ async function fetchFeedMessages(
+ feedId: string,
+ options?: {
+ cursor?: string;
+ since?: number;
+ limit?: number;
+ }
+ ): Promise<{ messages: FeedMessage[]; nextCursor?: string }> {
+ const requestId = nanoid();
+
+ const { promise, resolve, reject } = Promise_withResolvers<{
+ messages: FeedMessage[];
+ nextCursor?: string;
+ }>();
+
+ pendingFeedMessagesRequests.set(requestId, { resolve, reject });
+
+ const message: FetchFeedMessagesClientMsg = {
+ type: ClientMsgCode.FETCH_FEED_MESSAGES,
+ requestId,
+ feedId,
+ cursor: options?.cursor,
+ since: options?.since,
+ limit: options?.limit,
+ };
+
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+
+ setTimeout(() => {
+ if (pendingFeedMessagesRequests.has(requestId)) {
+ pendingFeedMessagesRequests.delete(requestId);
+ reject(new Error("Feed messages fetch timeout"));
+ }
+ }, FEEDS_TIMEOUT);
+
+ return promise;
+ }
+
+ function addFeed(
+ feedId: string,
+ options?: { metadata?: FeedCreateMetadata; createdAt?: number }
+ ): Promise {
+ const requestId = nanoid();
+ const promise = registerFeedMutation(requestId, "add-feed", feedId);
+ const message: AddFeedClientMsg = {
+ type: ClientMsgCode.ADD_FEED,
+ requestId,
+ feedId,
+ metadata: options?.metadata,
+ createdAt: options?.createdAt,
+ };
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+ return promise;
+ }
+
+ function updateFeed(
+ feedId: string,
+ metadata: FeedUpdateMetadata
+ ): Promise {
+ const requestId = nanoid();
+ const promise = registerFeedMutation(requestId, "update-feed", feedId);
+ const message: UpdateFeedClientMsg = {
+ type: ClientMsgCode.UPDATE_FEED,
+ requestId,
+ feedId,
+ metadata,
+ };
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+ return promise;
+ }
+
+ function deleteFeed(feedId: string): Promise {
+ const requestId = nanoid();
+ const promise = registerFeedMutation(requestId, "delete-feed", feedId);
+ const message: DeleteFeedClientMsg = {
+ type: ClientMsgCode.DELETE_FEED,
+ requestId,
+ feedId,
+ };
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+ return promise;
+ }
+
+ function addFeedMessage(
+ feedId: string,
+ data: JsonObject,
+ options?: { id?: string; createdAt?: number }
+ ): Promise {
+ const requestId = nanoid();
+ const promise = registerFeedMutation(requestId, "add-message", feedId, {
+ expectedClientMessageId: options?.id,
+ });
+ const message: AddFeedMessageClientMsg = {
+ type: ClientMsgCode.ADD_FEED_MESSAGE,
+ requestId,
+ feedId,
+ data,
+ id: options?.id,
+ createdAt: options?.createdAt,
+ };
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+ return promise;
+ }
+
+ function updateFeedMessage(
+ feedId: string,
+ messageId: string,
+ data: JsonObject,
+ options?: { updatedAt?: number }
+ ): Promise {
+ const requestId = nanoid();
+ const promise = registerFeedMutation(requestId, "update-message", feedId, {
+ messageId,
+ });
+ const message: UpdateFeedMessageClientMsg = {
+ type: ClientMsgCode.UPDATE_FEED_MESSAGE,
+ requestId,
+ feedId,
+ messageId,
+ data,
+ updatedAt: options?.updatedAt,
+ };
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+ return promise;
+ }
+
+ function deleteFeedMessage(feedId: string, messageId: string): Promise {
+ const requestId = nanoid();
+ const promise = registerFeedMutation(requestId, "delete-message", feedId, {
+ messageId,
+ });
+ const message: DeleteFeedMessageClientMsg = {
+ type: ClientMsgCode.DELETE_FEED_MESSAGE,
+ requestId,
+ feedId,
+ messageId,
+ };
+ context.buffer.messages.push(message);
+ flushNowOrSoon();
+ return promise;
+ }
+
function undo() {
if (context.activeBatch) {
throw new Error("undo is not allowed during a batch");
@@ -2777,6 +3402,7 @@ export function createRoom<
ydoc: eventHub.ydoc.observable,
comments: eventHub.comments.observable,
+ feeds: eventHub.feeds.observable,
roomWillDestroy: eventHub.roomWillDestroy.observable,
};
@@ -3080,7 +3706,7 @@ export function createRoom<
id: roomId,
subscribe: makeClassicSubscribeFn(
roomId,
- events,
+ eventHub,
config.errorEventSource
),
@@ -3088,6 +3714,12 @@ export function createRoom<
reconnect: () => managedSocket.reconnect(),
disconnect: () => managedSocket.disconnect(),
destroy: () => {
+ pendingFeedsRequests.forEach((request) =>
+ request.reject(new Error("Room destroyed"))
+ );
+ pendingFeedMessagesRequests.forEach((request) =>
+ request.reject(new Error("Room destroyed"))
+ );
// remove the roomWillDestroy event from the event hub
const { roomWillDestroy, ...eventsExceptDestroy } = eventHub;
// Unregister all registered callbacks
@@ -3123,6 +3755,14 @@ export function createRoom<
},
fetchYDoc,
+ fetchFeeds,
+ fetchFeedMessages,
+ addFeed,
+ updateFeed,
+ deleteFeed,
+ addFeedMessage,
+ updateFeedMessage,
+ deleteFeedMessage,
getStorage,
getStorageSnapshot,
getStorageStatus,
diff --git a/packages/liveblocks-core/src/types/LiveblocksError.ts b/packages/liveblocks-core/src/types/LiveblocksError.ts
index b63af2a924e..24231467aa4 100644
--- a/packages/liveblocks-core/src/types/LiveblocksError.ts
+++ b/packages/liveblocks-core/src/types/LiveblocksError.ts
@@ -20,6 +20,14 @@ type LargeMessageErrorContext = {
type: "LARGE_MESSAGE_ERROR";
};
+type FeedRequestErrorContext = {
+ type: "FEED_REQUEST_ERROR";
+ roomId: string;
+ requestId: string;
+ code: string;
+ reason?: string;
+};
+
// All possible errors originating from using Comments or Notifications
type CommentsOrNotificationsErrorContext =
| {
@@ -106,6 +114,7 @@ export type LiveblocksErrorContext = Relax<
| CommentsOrNotificationsErrorContext // from Comments or Notifications or UserNotificationSettings
| AiConnectionErrorContext // from AI
| LargeMessageErrorContext // whena message is too large
+ | FeedRequestErrorContext // feed WebSocket mutations
>;
export class LiveblocksError extends Error {
@@ -183,6 +192,9 @@ function defaultMessageFromContext(context: LiveblocksErrorContext): string {
case "UPDATE_NOTIFICATION_SETTINGS_ERROR": return "Could not update notification settings";
case "LARGE_MESSAGE_ERROR": return "Could not send large message";
+ case "FEED_REQUEST_ERROR":
+ return context.reason ?? "Feed request failed";
+
default:
return assertNever(context, "Unhandled case");
}
diff --git a/packages/liveblocks-emails/package.json b/packages/liveblocks-emails/package.json
index 9f9d007f477..fe9259c4b8d 100644
--- a/packages/liveblocks-emails/package.json
+++ b/packages/liveblocks-emails/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/emails",
- "version": "3.15.5",
+ "version": "3.16.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.",
@@ -37,8 +37,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.15.5",
- "@liveblocks/node": "3.15.5"
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/node": "3.16.0"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc"
diff --git a/packages/liveblocks-node-lexical/package.json b/packages/liveblocks-node-lexical/package.json
index 299060f5e08..d25e1ee6ecb 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.15.5",
+ "version": "3.16.0",
"description": "A server-side utility that lets you modify lexical documents hosted in Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
@@ -36,8 +36,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.15.5",
- "@liveblocks/node": "3.15.5",
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/node": "3.16.0",
"yjs": "^13.6.18"
},
"peerDependencies": {
diff --git a/packages/liveblocks-node-prosemirror/package.json b/packages/liveblocks-node-prosemirror/package.json
index 7b2e7d6645e..b345a34f6b3 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.15.5",
+ "version": "3.16.0",
"description": "A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
@@ -36,8 +36,8 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.15.5",
- "@liveblocks/node": "3.15.5",
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/node": "3.16.0",
"yjs": "^13.6.20"
},
"peerDependencies": {
diff --git a/packages/liveblocks-node/package.json b/packages/liveblocks-node/package.json
index dab95d2bd66..c2dd1a8e8e4 100644
--- a/packages/liveblocks-node/package.json
+++ b/packages/liveblocks-node/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node",
- "version": "3.15.5",
+ "version": "3.16.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.",
@@ -36,7 +36,7 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/core": "3.15.5",
+ "@liveblocks/core": "3.16.0",
"@stablelib/base64": "^1.0.1",
"fast-sha256": "^1.3.0",
"node-fetch": "^2.6.1"
diff --git a/packages/liveblocks-node/src/Session.ts b/packages/liveblocks-node/src/Session.ts
index 0ca6ce1a4ef..521eb9540d1 100644
--- a/packages/liveblocks-node/src/Session.ts
+++ b/packages/liveblocks-node/src/Session.ts
@@ -17,6 +17,7 @@ const ALL_PERMISSIONS = Object.freeze([
"room:presence:write",
"comments:write",
"comments:read",
+ "feeds:write",
] as const);
export type Permission = (typeof ALL_PERMISSIONS)[number];
@@ -34,15 +35,15 @@ const MAX_PERMS_PER_SET = 10;
*/
const READ_ACCESS = Object.freeze([
"room:read",
- "room:presence:write",
- "comments:read",
+ "room:presence:write", // TODO: Remove once backend no longer requires this
+ "comments:read", // TODO: Remove — implied by room:read
] as const);
/**
* Assign this to a room (or wildcard pattern) if you want to grant the user
* permissions to read and write to the room's storage and comments.
*/
-const FULL_ACCESS = Object.freeze(["room:write", "comments:write"] as const);
+const FULL_ACCESS = Object.freeze(["room:write"] as const);
const roomPatternRegex = /^([*]|[^*]{1,128}[*]?)$/;
diff --git a/packages/liveblocks-node/src/__tests__/Session.test.ts b/packages/liveblocks-node/src/__tests__/Session.test.ts
index 9219a862db0..254ab596efe 100644
--- a/packages/liveblocks-node/src/__tests__/Session.test.ts
+++ b/packages/liveblocks-node/src/__tests__/Session.test.ts
@@ -69,7 +69,7 @@ describe("authorization (new API)", () => {
expect(
session.allow("xyz", session.FULL_ACCESS).serializePermissions()
).toEqual({
- xyz: ["room:write", "comments:write"],
+ xyz: ["room:write"],
});
});
diff --git a/packages/liveblocks-node/src/__tests__/client.test.ts b/packages/liveblocks-node/src/__tests__/client.test.ts
index 61473468a64..33d70eb3ee1 100644
--- a/packages/liveblocks-node/src/__tests__/client.test.ts
+++ b/packages/liveblocks-node/src/__tests__/client.test.ts
@@ -1,6 +1,8 @@
import type {
CommentData,
CommentUserReaction,
+ Feed,
+ FeedMessage,
NotificationSettingsPlain,
RoomSubscriptionSettings,
StorageNode,
@@ -687,7 +689,7 @@ describe("client", () => {
avatar: "https://example.com/avatar.png",
},
ttl: 60,
- }),
+ })
).resolves.toBeUndefined();
});
@@ -699,7 +701,7 @@ describe("client", () => {
userId: "agent-ai",
data: { status: "active" },
userInfo: { name: "AI Assistant" },
- }),
+ })
).resolves.toBeUndefined();
});
@@ -708,9 +710,9 @@ describe("client", () => {
http.post(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/presence`, () => {
return HttpResponse.json(
{ error: "INVALID_REQUEST", message: "Invalid presence data" },
- { status: 422 },
+ { status: 422 }
);
- }),
+ })
);
const client = new Liveblocks({ secret: "sk_xxx" });
@@ -4774,5 +4776,339 @@ describe("client", () => {
}
});
});
+
+ describe("feeds", () => {
+ const feed: Feed = {
+ feedId: "feed_123",
+ metadata: { key: "value" },
+ createdAt: 1234567890,
+ updatedAt: 1234567890,
+ };
+
+ const feedMessage: FeedMessage = {
+ id: "msg_123",
+ createdAt: 1234567890,
+ updatedAt: 1234567890,
+ data: { content: "Hello" },
+ };
+
+ describe("getFeeds", () => {
+ test("should return a list of feeds", async () => {
+ server.use(
+ http.get(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds`, () => {
+ return HttpResponse.json({ data: [feed] }, { status: 200 });
+ })
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.getFeeds({ roomId: "room_123" })
+ ).resolves.toEqual({ data: [feed] });
+ });
+
+ test("should throw a LiveblocksError on error response", async () => {
+ server.use(
+ http.get(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds`, () => {
+ return HttpResponse.json(
+ { message: "Room not found" },
+ { status: 404 }
+ );
+ })
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ try {
+ await client.getFeeds({ roomId: "nonexistent" });
+ expect(true).toBe(false);
+ } catch (err) {
+ expect(err instanceof LiveblocksError).toBe(true);
+ if (err instanceof LiveblocksError) {
+ expect(err.status).toBe(404);
+ }
+ }
+ });
+ });
+
+ describe("createFeed", () => {
+ test("should create a feed", async () => {
+ server.use(
+ http.post(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds`, () => {
+ return HttpResponse.json(feed, { status: 200 });
+ })
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.createFeed({
+ roomId: "room_123",
+ feedId: "feed_123",
+ metadata: { key: "value" },
+ })
+ ).resolves.toEqual(feed);
+ });
+
+ test("should create a feed without metadata", async () => {
+ server.use(
+ http.post(`${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds`, () => {
+ return HttpResponse.json(feed, { status: 200 });
+ })
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.createFeed({
+ roomId: "room_123",
+ feedId: "feed_123",
+ })
+ ).resolves.toEqual(feed);
+ });
+
+ test("should send createdAt as timestamp in the request body", async () => {
+ server.use(
+ http.post(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds`,
+ async ({ request }) => {
+ expect(await request.json()).toEqual({
+ feedId: "feed_123",
+ metadata: { key: "value" },
+ timestamp: 99_000,
+ });
+ return HttpResponse.json(feed, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await client.createFeed({
+ roomId: "room_123",
+ feedId: "feed_123",
+ metadata: { key: "value" },
+ createdAt: 99_000,
+ });
+ });
+ });
+
+ describe("getFeed", () => {
+ test("should return a feed", async () => {
+ server.use(
+ http.get(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId`,
+ () => {
+ return HttpResponse.json(feed, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.getFeed({
+ roomId: "room_123",
+ feedId: "feed_123",
+ })
+ ).resolves.toEqual(feed);
+ });
+ });
+
+ describe("updateFeed", () => {
+ test("should update feed metadata and return the updated feed", async () => {
+ const updatedFeed = {
+ ...feed,
+ metadata: { updated: "metadata" },
+ };
+ server.use(
+ http.patch(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId`,
+ () => {
+ return HttpResponse.json(updatedFeed, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.updateFeed({
+ roomId: "room_123",
+ feedId: "feed_123",
+ metadata: { updated: "metadata" },
+ })
+ ).resolves.toEqual(updatedFeed);
+ });
+ });
+
+ describe("deleteFeed", () => {
+ test("should delete a feed", async () => {
+ server.use(
+ http.delete(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId`,
+ () => {
+ return new HttpResponse(null, { status: 204 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.deleteFeed({
+ roomId: "room_123",
+ feedId: "feed_123",
+ })
+ ).resolves.toBeUndefined();
+ });
+ });
+
+ describe("getFeedMessages", () => {
+ test("should return a list of feed messages", async () => {
+ server.use(
+ http.get(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId/messages`,
+ () => {
+ return HttpResponse.json(
+ { data: [feedMessage] },
+ { status: 200 }
+ );
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.getFeedMessages({
+ roomId: "room_123",
+ feedId: "feed_123",
+ })
+ ).resolves.toEqual({ data: [feedMessage] });
+ });
+ });
+
+ describe("createFeedMessage", () => {
+ test("should create a feed message", async () => {
+ server.use(
+ http.post(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId/messages`,
+ () => {
+ return HttpResponse.json(feedMessage, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.createFeedMessage({
+ roomId: "room_123",
+ feedId: "feed_123",
+ data: { content: "Hello" },
+ })
+ ).resolves.toEqual(feedMessage);
+ });
+
+ test("should create a feed message with id and createdAt", async () => {
+ server.use(
+ http.post(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId/messages`,
+ async ({ request }) => {
+ expect(await request.json()).toEqual({
+ data: { content: "Hello" },
+ id: "msg_123",
+ timestamp: 1234567890,
+ });
+ return HttpResponse.json(feedMessage, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.createFeedMessage({
+ roomId: "room_123",
+ feedId: "feed_123",
+ id: "msg_123",
+ createdAt: 1234567890,
+ data: { content: "Hello" },
+ })
+ ).resolves.toEqual(feedMessage);
+ });
+ });
+
+ describe("updateFeedMessage", () => {
+ test("should update a feed message and return the updated message", async () => {
+ const updatedMessage = {
+ ...feedMessage,
+ data: { content: "Updated" },
+ updatedAt: 1234567891,
+ };
+ server.use(
+ http.patch(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId/messages/:messageId`,
+ () => {
+ return HttpResponse.json(updatedMessage, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.updateFeedMessage({
+ roomId: "room_123",
+ feedId: "feed_123",
+ messageId: "msg_123",
+ data: { content: "Updated" },
+ })
+ ).resolves.toEqual(updatedMessage);
+ });
+
+ test("should send updatedAt as timestamp in the request body", async () => {
+ const updatedMessage = {
+ ...feedMessage,
+ data: { content: "Updated" },
+ updatedAt: 42_000,
+ };
+ server.use(
+ http.patch(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId/messages/:messageId`,
+ async ({ request }) => {
+ expect(await request.json()).toEqual({
+ data: { content: "Updated" },
+ timestamp: 42_000,
+ });
+ return HttpResponse.json(updatedMessage, { status: 200 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.updateFeedMessage({
+ roomId: "room_123",
+ feedId: "feed_123",
+ messageId: "msg_123",
+ data: { content: "Updated" },
+ updatedAt: 42_000,
+ })
+ ).resolves.toEqual(updatedMessage);
+ });
+ });
+
+ describe("deleteFeedMessage", () => {
+ test("should delete a feed message", async () => {
+ server.use(
+ http.delete(
+ `${DEFAULT_BASE_URL}/v2/rooms/:roomId/feeds/:feedId/messages/:messageId`,
+ () => {
+ return new HttpResponse(null, { status: 204 });
+ }
+ )
+ );
+
+ const client = new Liveblocks({ secret: "sk_xxx" });
+ await expect(
+ client.deleteFeedMessage({
+ roomId: "room_123",
+ feedId: "feed_123",
+ messageId: "msg_123",
+ })
+ ).resolves.toBeUndefined();
+ });
+ });
+ });
});
});
diff --git a/packages/liveblocks-node/src/client.ts b/packages/liveblocks-node/src/client.ts
index 4072e8387af..ea2275d0ded 100644
--- a/packages/liveblocks-node/src/client.ts
+++ b/packages/liveblocks-node/src/client.ts
@@ -17,9 +17,15 @@ import type {
DAD,
DCM,
DE,
+ DFM,
+ DFMD,
DS,
DTM,
DU,
+ Feed,
+ FeedCreateMetadata,
+ FeedMessage,
+ FeedUpdateMetadata,
GroupData,
GroupDataPlain,
GroupScopes,
@@ -620,6 +626,30 @@ export type GetWebKnowledgeSourceLinksOptions = {
knowledgeSourceId: string;
} & PaginationOptions;
+export type CreateFeedOptions = {
+ feedId: string;
+ metadata?: FeedCreateMetadata;
+ /** Creation time in ms; serialized as `timestamp` in the REST request body. */
+ createdAt?: number;
+};
+
+export type UpdateFeedOptions = {
+ metadata: FeedUpdateMetadata;
+};
+
+export type CreateFeedMessageOptions = {
+ id?: string;
+ /** Creation time in ms; serialized as `timestamp` in the REST request body. */
+ createdAt?: number;
+ data: FMD;
+};
+
+export type UpdateFeedMessageOptions = {
+ data: FMD;
+ /** Update time in ms; serialized as `timestamp` in the REST request body. */
+ updatedAt?: number;
+};
+
type KnowledgeSourcePlain = DateToString;
export type KnowledgeSource = (
@@ -760,6 +790,27 @@ export class Liveblocks {
return res;
}
+ async #patch(
+ path: URLSafeString,
+ json: Json,
+ options?: RequestOptions
+ ): Promise {
+ const url = urljoin(this.#baseUrl, path);
+ const headers = {
+ Authorization: `Bearer ${this.#secret}`,
+ "Content-Type": "application/json",
+ };
+ const fetch = await fetchPolyfill();
+ const res = await fetch(url, {
+ method: "PATCH",
+ headers,
+ body: JSON.stringify(json),
+ signal: options?.signal,
+ });
+ xwarn(res, "PATCH", path);
+ return res;
+ }
+
async #putBinary(
path: URLSafeString,
body: Uint8Array,
@@ -3333,6 +3384,247 @@ export class Liveblocks {
data: page.data.map(inflateWebKnowledgeSourceLink),
};
}
+
+ /* -------------------------------------------------------------------------------------------------
+ * Feeds
+ * -----------------------------------------------------------------------------------------------*/
+
+ /**
+ * Returns a list of feeds in a room.
+ * @param params.roomId The room ID to get the feeds from.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns A list of feeds.
+ */
+ public async getFeeds(
+ params: { roomId: string },
+ options?: RequestOptions
+ ): Promise<{ data: Feed[] }> {
+ const { roomId } = params;
+ const res = await this.#get(
+ url`/v2/rooms/${roomId}/feeds`,
+ undefined,
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as { data: Feed[] };
+ }
+
+ /**
+ * Creates a new feed in a room.
+ * @param params.roomId The room ID to create the feed in.
+ * @param params.feedId The feed ID.
+ * @param params.metadata (optional) The metadata for the feed.
+ * @param params.createdAt (optional) Creation time in ms. Sent to the API as `timestamp`. If not provided, the server uses the current time.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns The created feed.
+ */
+ public async createFeed(
+ params: { roomId: string } & CreateFeedOptions,
+ options?: RequestOptions
+ ): Promise> {
+ const { roomId, feedId, metadata, createdAt } = params;
+ const res = await this.#post(
+ url`/v2/rooms/${roomId}/feeds`,
+ {
+ feedId,
+ ...(metadata !== undefined ? { metadata } : {}),
+ ...(createdAt !== undefined ? { timestamp: createdAt } : {}),
+ },
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as Feed;
+ }
+
+ /**
+ * Returns a feed with the given id.
+ * @param params.roomId The room ID to get the feed from.
+ * @param params.feedId The feed ID.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns The feed.
+ */
+ public async getFeed(
+ params: { roomId: string; feedId: string },
+ options?: RequestOptions
+ ): Promise> {
+ const { roomId, feedId } = params;
+ const res = await this.#get(
+ url`/v2/rooms/${roomId}/feeds/${feedId}`,
+ undefined,
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as Feed;
+ }
+
+ /**
+ * Updates the metadata of a feed.
+ * @param params.roomId The room ID to update the feed in.
+ * @param params.feedId The feed ID to update.
+ * @param params.metadata The metadata for the feed.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns The updated feed.
+ */
+ public async updateFeed(
+ params: {
+ roomId: string;
+ feedId: string;
+ } & UpdateFeedOptions,
+ options?: RequestOptions
+ ): Promise> {
+ const { roomId, feedId, metadata } = params;
+ const res = await this.#patch(
+ url`/v2/rooms/${roomId}/feeds/${feedId}`,
+ { metadata },
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as Feed;
+ }
+
+ /**
+ * Deletes a feed.
+ * @param params.roomId The room ID to delete the feed from.
+ * @param params.feedId The feed ID to delete.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ */
+ public async deleteFeed(
+ params: { roomId: string; feedId: string },
+ options?: RequestOptions
+ ): Promise {
+ const { roomId, feedId } = params;
+ const res = await this.#delete(
+ url`/v2/rooms/${roomId}/feeds/${feedId}`,
+ undefined,
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ }
+
+ /**
+ * Returns a list of messages in a feed.
+ * @param params.roomId The room ID to get the feed messages from.
+ * @param params.feedId The feed ID to get the messages from.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns A list of feed messages.
+ */
+ public async getFeedMessages(
+ params: { roomId: string; feedId: string },
+ options?: RequestOptions
+ ): Promise<{ data: FeedMessage[] }> {
+ const { roomId, feedId } = params;
+ const res = await this.#get(
+ url`/v2/rooms/${roomId}/feeds/${feedId}/messages`,
+ undefined,
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as { data: FeedMessage[] };
+ }
+
+ /**
+ * Creates a new message in a feed.
+ * @param params.roomId The room ID to create the feed message in.
+ * @param params.feedId The feed ID to create the message in.
+ * @param params.id (optional) The message ID. If not provided, one will be generated.
+ * @param params.createdAt (optional) Creation time in ms. Sent to the API as `timestamp`. If not provided, the server uses the current time.
+ * @param params.data The message data.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns The created feed message.
+ */
+ public async createFeedMessage(
+ params: {
+ roomId: string;
+ feedId: string;
+ } & CreateFeedMessageOptions,
+ options?: RequestOptions
+ ): Promise> {
+ const { roomId, feedId, id, createdAt, data } = params;
+ const res = await this.#post(
+ url`/v2/rooms/${roomId}/feeds/${feedId}/messages`,
+ {
+ data,
+ ...(id !== undefined ? { id } : {}),
+ ...(createdAt !== undefined ? { timestamp: createdAt } : {}),
+ },
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as FeedMessage;
+ }
+
+ /**
+ * Updates a feed message.
+ * @param params.roomId The room ID to update the feed message in.
+ * @param params.feedId The feed ID to update the message in.
+ * @param params.messageId The message ID to update.
+ * @param params.data The message data.
+ * @param params.updatedAt (optional) Update time in ms. Sent to the API as `timestamp`. If omitted, the server uses the current time.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ * @returns The updated feed message.
+ */
+ public async updateFeedMessage(
+ params: {
+ roomId: string;
+ feedId: string;
+ messageId: string;
+ } & UpdateFeedMessageOptions,
+ options?: RequestOptions
+ ): Promise> {
+ const { roomId, feedId, messageId, data, updatedAt } = params;
+ const res = await this.#patch(
+ url`/v2/rooms/${roomId}/feeds/${feedId}/messages/${messageId}`,
+ {
+ data,
+ ...(updatedAt !== undefined ? { timestamp: updatedAt } : {}),
+ },
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ return (await res.json()) as FeedMessage;
+ }
+
+ /**
+ * Deletes a feed message.
+ * @param params.roomId The room ID to delete the feed message from.
+ * @param params.feedId The feed ID to delete the message from.
+ * @param params.messageId The message ID to delete.
+ * @param options.signal (optional) An abort signal to cancel the request.
+ */
+ public async deleteFeedMessage(
+ params: {
+ roomId: string;
+ feedId: string;
+ messageId: string;
+ },
+ options?: RequestOptions
+ ): Promise {
+ const { roomId, feedId, messageId } = params;
+ const res = await this.#delete(
+ url`/v2/rooms/${roomId}/feeds/${feedId}/messages/${messageId}`,
+ undefined,
+ options
+ );
+ if (!res.ok) {
+ throw await LiveblocksError.from(res);
+ }
+ }
}
export class LiveblocksError extends Error {
diff --git a/packages/liveblocks-node/src/index.ts b/packages/liveblocks-node/src/index.ts
index ff6620d6c07..7f778a6657e 100644
--- a/packages/liveblocks-node/src/index.ts
+++ b/packages/liveblocks-node/src/index.ts
@@ -7,6 +7,8 @@ detectDupes(PKG_NAME, PKG_VERSION, PKG_FORMAT);
export type {
AiCopilot,
CreateAiCopilotOptions,
+ CreateFeedMessageOptions,
+ CreateFeedOptions,
CreateFileKnowledgeSourceOptions,
CreateRoomOptions,
CreateWebKnowledgeSourceOptions,
@@ -32,6 +34,8 @@ export type {
SetPresenceOptions,
ThreadParticipants,
UpdateAiCopilotOptions,
+ UpdateFeedMessageOptions,
+ UpdateFeedOptions,
UpdateRoomOptions,
UpsertRoomOptions,
WebKnowledgeSourceLink,
diff --git a/packages/liveblocks-python-codegen/config.yaml b/packages/liveblocks-python-codegen/config.yaml
index e8a6956bccd..4700594b389 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.15.5
+package_version_override: 3.16.0
post_hooks:
- "uvx ruff check --fix-only ."
diff --git a/packages/liveblocks-python/README.md b/packages/liveblocks-python/README.md
index 019c3811a55..3967fe48966 100644
--- a/packages/liveblocks-python/README.md
+++ b/packages/liveblocks-python/README.md
@@ -1200,6 +1200,245 @@ print(result)
| `thread_id` | `str` | Yes | ID of the thread |
+---
+
+### Feeds
+
+#### `get_feeds`
+
+This endpoint returns the feeds in the requested room. Corresponds to [`liveblocks.getFeeds`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-feeds).
+
+**Example**
+```python
+result = client.get_feeds(
+ room_id="my-room-id",
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # since=1660000988137,
+ # limit=20,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `cursor` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+| `since` | `int \| Unset` | No | Only return feeds with `createdAt` greater than this Unix timestamp in milliseconds. |
+| `limit` | `int \| Unset` | No | A limit on the number of feeds to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+
+
+---
+
+#### `create_feed`
+
+This endpoint creates a new feed in a room. Corresponds to [`liveblocks.createFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-feeds).
+
+**Example**
+```python
+from liveblocks.models import CreateFeedRequestBody
+
+result = client.create_feed(
+ room_id="my-room-id",
+ body=CreateFeedRequestBody(
+ feed_id="...",
+ # metadata=...,
+ # timestamp=0.0,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `body` | `CreateFeedRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_feed`
+
+This endpoint returns a feed by its ID. Corresponds to [`liveblocks.getFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-feeds-feedId).
+
+**Example**
+```python
+result = client.get_feed(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+
+
+---
+
+#### `delete_feed`
+
+This endpoint deletes a feed. Corresponds to [`liveblocks.deleteFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-feeds-feedId).
+
+**Example**
+```python
+client.delete_feed(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+
+
+---
+
+#### `update_feed`
+
+This endpoint updates the metadata of a feed. Corresponds to [`liveblocks.updateFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-rooms-roomId-feeds-feedId).
+
+**Example**
+```python
+from liveblocks.models import UpdateFeedRequestBody
+
+result = client.update_feed(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ body=UpdateFeedRequestBody(
+ metadata=...,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+| `body` | `UpdateFeedRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `get_feed_messages`
+
+This endpoint returns the messages in a feed. Corresponds to [`liveblocks.getFeedMessages`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-feeds-feedId-messages).
+
+**Example**
+```python
+result = client.get_feed_messages(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # since=1660000988137,
+ # limit=20,
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+| `cursor` | `str \| Unset` | No | A cursor used for pagination. Get the value from the `nextCursor` response of the previous page. |
+| `since` | `int \| Unset` | No | Only return messages with `createdAt` greater than this Unix timestamp in milliseconds. |
+| `limit` | `int \| Unset` | No | A limit on the number of messages to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)* |
+
+
+---
+
+#### `create_feed_message`
+
+This endpoint creates a new message in a feed. Corresponds to [`liveblocks.createFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-feeds-feedId-messages).
+
+**Example**
+```python
+from liveblocks.models import CreateFeedMessageRequestBody
+
+result = client.create_feed_message(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ body=CreateFeedMessageRequestBody(
+ data=...,
+ # id="...",
+ # timestamp=0.0,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+| `body` | `CreateFeedMessageRequestBody` | Yes | Request body (application/json) |
+
+
+---
+
+#### `delete_feed_message`
+
+This endpoint deletes a feed message. Corresponds to [`liveblocks.deleteFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-feeds-feedId-messages-messageId).
+
+**Example**
+```python
+client.delete_feed_message(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ message_id="msg_xyz789",
+)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+| `message_id` | `str` | Yes | ID of the message |
+
+
+---
+
+#### `update_feed_message`
+
+This endpoint updates a feed message. Corresponds to [`liveblocks.updateFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-rooms-roomId-feeds-feedId-messages-messageId).
+
+**Example**
+```python
+from liveblocks.models import UpdateFeedMessageRequestBody
+
+result = client.update_feed_message(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ message_id="msg_xyz789",
+ body=UpdateFeedMessageRequestBody(
+ data=...,
+ # timestamp=0.0,
+ ),
+)
+print(result)
+```
+**Parameters:**
+
+| Name | Type | Required | Description |
+|------|------|----------|-------------|
+| `room_id` | `str` | Yes | ID of the room |
+| `feed_id` | `str` | Yes | ID of the feed |
+| `message_id` | `str` | Yes | ID of the message |
+| `body` | `UpdateFeedMessageRequestBody` | Yes | Request body (application/json) |
+
+
---
### Auth
diff --git a/packages/liveblocks-python/README.mdx b/packages/liveblocks-python/README.mdx
index 1d48a82a63c..a802640fa64 100644
--- a/packages/liveblocks-python/README.mdx
+++ b/packages/liveblocks-python/README.mdx
@@ -1620,6 +1620,371 @@ print(result)
+## Feeds
+
+### get_feeds
+
+This endpoint returns the feeds in the requested room. Corresponds to [`liveblocks.getFeeds`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-feeds).
+
+```python
+result = client.get_feeds(
+ room_id="my-room-id",
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # since=1660000988137,
+ # limit=20,
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+ Only return feeds with `createdAt` greater than this Unix timestamp in milliseconds.
+
+
+ A limit on the number of feeds to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+
+
+### create_feed
+
+This endpoint creates a new feed in a room. Corresponds to [`liveblocks.createFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-feeds).
+
+```python
+from liveblocks.models import CreateFeedRequestBody
+
+result = client.create_feed(
+ room_id="my-room-id",
+ body=CreateFeedRequestBody(
+ feed_id="...",
+ # metadata=...,
+ # timestamp=0.0,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_feed
+
+This endpoint returns a feed by its ID. Corresponds to [`liveblocks.getFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-feeds-feedId).
+
+```python
+result = client.get_feed(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+
+
+### delete_feed
+
+This endpoint deletes a feed. Corresponds to [`liveblocks.deleteFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-feeds-feedId).
+
+```python
+client.delete_feed(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+
+
+### update_feed
+
+This endpoint updates the metadata of a feed. Corresponds to [`liveblocks.updateFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-rooms-roomId-feeds-feedId).
+
+```python
+from liveblocks.models import UpdateFeedRequestBody
+
+result = client.update_feed(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ body=UpdateFeedRequestBody(
+ metadata=...,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### get_feed_messages
+
+This endpoint returns the messages in a feed. Corresponds to [`liveblocks.getFeedMessages`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-feeds-feedId-messages).
+
+```python
+result = client.get_feed_messages(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ # cursor="eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9",
+ # since=1660000988137,
+ # limit=20,
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+ A cursor used for pagination. Get the value from the `nextCursor` response of the previous page.
+
+
+ Only return messages with `createdAt` greater than this Unix timestamp in milliseconds.
+
+
+ A limit on the number of messages to be returned. The limit can range between 1 and 100, and defaults to 20. *(default: `20`)*
+
+
+
+
+### create_feed_message
+
+This endpoint creates a new message in a feed. Corresponds to [`liveblocks.createFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-roomId-feeds-feedId-messages).
+
+```python
+from liveblocks.models import CreateFeedMessageRequestBody
+
+result = client.create_feed_message(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ body=CreateFeedMessageRequestBody(
+ data=...,
+ # id="...",
+ # timestamp=0.0,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+ Request body (application/json).
+
+
+
+
+
+### delete_feed_message
+
+This endpoint deletes a feed message. Corresponds to [`liveblocks.deleteFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-roomId-feeds-feedId-messages-messageId).
+
+```python
+client.delete_feed_message(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ message_id="msg_xyz789",
+)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+ ID of the message
+
+
+
+
+
+### update_feed_message
+
+This endpoint updates a feed message. Corresponds to [`liveblocks.updateFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-rooms-roomId-feeds-feedId-messages-messageId).
+
+```python
+from liveblocks.models import UpdateFeedMessageRequestBody
+
+result = client.update_feed_message(
+ room_id="my-room-id",
+ feed_id="fd_abc123",
+ message_id="msg_xyz789",
+ body=UpdateFeedMessageRequestBody(
+ data=...,
+ # timestamp=0.0,
+ ),
+)
+print(result)
+```
+
+
+
+ ID of the room
+
+
+
+ ID of the feed
+
+
+
+ ID of the message
+
+
+
+ Request body (application/json).
+
+
+
+
+
## Auth
### authorize_user
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/__init__.py b/packages/liveblocks-python/liveblocks/api/feeds/__init__.py
new file mode 100644
index 00000000000..2d7c0b23da3
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/__init__.py
@@ -0,0 +1 @@
+"""Contains endpoint functions for accessing the API"""
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/create_feed.py b/packages/liveblocks-python/liveblocks/api/feeds/create_feed.py
new file mode 100644
index 00000000000..50be7e83463
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/create_feed.py
@@ -0,0 +1,74 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_feed_request_body import CreateFeedRequestBody
+from ...models.feed import Feed
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ body: CreateFeedRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/feeds".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Feed:
+ if response.status_code == 200:
+ response_200 = Feed.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ body: CreateFeedRequestBody,
+) -> Feed:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: CreateFeedRequestBody,
+) -> Feed:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/create_feed_message.py b/packages/liveblocks-python/liveblocks/api/feeds/create_feed_message.py
new file mode 100644
index 00000000000..eabd06de4ed
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/create_feed_message.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.create_feed_message_request_body import CreateFeedMessageRequestBody
+from ...models.feed_message import FeedMessage
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+ *,
+ body: CreateFeedMessageRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "post",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}/messages".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> FeedMessage:
+ if response.status_code == 200:
+ response_200 = FeedMessage.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.Client,
+ body: CreateFeedMessageRequestBody,
+) -> FeedMessage:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: CreateFeedMessageRequestBody,
+) -> FeedMessage:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/delete_feed.py b/packages/liveblocks-python/liveblocks/api/feeds/delete_feed.py
new file mode 100644
index 00000000000..237eba06b53
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/delete_feed.py
@@ -0,0 +1,64 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/delete_feed_message.py b/packages/liveblocks-python/liveblocks/api/feeds/delete_feed_message.py
new file mode 100644
index 00000000000..51d71161dd9
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/delete_feed_message.py
@@ -0,0 +1,70 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "delete",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}/messages/{message_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ message_id=quote(str(message_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> None:
+ if response.status_code == 204:
+ return None
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ client: httpx.Client,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> None:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/get_feed.py b/packages/liveblocks-python/liveblocks/api/feeds/get_feed.py
new file mode 100644
index 00000000000..863da946c85
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/get_feed.py
@@ -0,0 +1,67 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.feed import Feed
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+) -> dict[str, Any]:
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ ),
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Feed:
+ if response.status_code == 200:
+ response_200 = Feed.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.Client,
+) -> Feed:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.AsyncClient,
+) -> Feed:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/get_feed_messages.py b/packages/liveblocks-python/liveblocks/api/feeds/get_feed_messages.py
new file mode 100644
index 00000000000..2e418fb899c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/get_feed_messages.py
@@ -0,0 +1,95 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_feed_messages_response import GetFeedMessagesResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+ *,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["cursor"] = cursor
+
+ params["since"] = since
+
+ params["limit"] = limit
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}/messages".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetFeedMessagesResponse:
+ if response.status_code == 200:
+ response_200 = GetFeedMessagesResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.Client,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+) -> GetFeedMessagesResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.AsyncClient,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+) -> GetFeedMessagesResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/get_feeds.py b/packages/liveblocks-python/liveblocks/api/feeds/get_feeds.py
new file mode 100644
index 00000000000..5274c934788
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/get_feeds.py
@@ -0,0 +1,89 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.get_feeds_response import GetFeedsResponse
+from ...types import UNSET, Unset
+
+
+def _get_kwargs(
+ room_id: str,
+ *,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+) -> dict[str, Any]:
+
+ params: dict[str, Any] = {}
+
+ params["cursor"] = cursor
+
+ params["since"] = since
+
+ params["limit"] = limit
+
+ params = {k: v for k, v in params.items() if v is not UNSET and v is not None}
+
+ _kwargs: dict[str, Any] = {
+ "method": "get",
+ "url": "/v2/rooms/{room_id}/feeds".format(
+ room_id=quote(str(room_id), safe=""),
+ ),
+ "params": params,
+ }
+
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> GetFeedsResponse:
+ if response.status_code == 200:
+ response_200 = GetFeedsResponse.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ *,
+ client: httpx.Client,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+) -> GetFeedsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ *,
+ client: httpx.AsyncClient,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+) -> GetFeedsResponse:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/update_feed.py b/packages/liveblocks-python/liveblocks/api/feeds/update_feed.py
new file mode 100644
index 00000000000..7d5b26f0954
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/update_feed.py
@@ -0,0 +1,80 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.feed import Feed
+from ...models.update_feed_request_body import UpdateFeedRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+ *,
+ body: UpdateFeedRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "patch",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> Feed:
+ if response.status_code == 200:
+ response_200 = Feed.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateFeedRequestBody,
+) -> Feed:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateFeedRequestBody,
+) -> Feed:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/api/feeds/update_feed_message.py b/packages/liveblocks-python/liveblocks/api/feeds/update_feed_message.py
new file mode 100644
index 00000000000..51d343ee55d
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/api/feeds/update_feed_message.py
@@ -0,0 +1,86 @@
+from typing import Any
+from urllib.parse import quote
+
+import httpx
+
+from ... import errors
+from ...models.feed_message import FeedMessage
+from ...models.update_feed_message_request_body import UpdateFeedMessageRequestBody
+
+
+def _get_kwargs(
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ body: UpdateFeedMessageRequestBody,
+) -> dict[str, Any]:
+ headers: dict[str, Any] = {}
+
+ _kwargs: dict[str, Any] = {
+ "method": "patch",
+ "url": "/v2/rooms/{room_id}/feeds/{feed_id}/messages/{message_id}".format(
+ room_id=quote(str(room_id), safe=""),
+ feed_id=quote(str(feed_id), safe=""),
+ message_id=quote(str(message_id), safe=""),
+ ),
+ }
+
+ _kwargs["json"] = body.to_dict()
+
+ headers["Content-Type"] = "application/json"
+
+ _kwargs["headers"] = headers
+ return _kwargs
+
+
+def _parse_response(*, response: httpx.Response) -> FeedMessage:
+ if response.status_code == 200:
+ response_200 = FeedMessage.from_dict(response.json())
+
+ return response_200
+
+ raise errors.LiveblocksError.from_response(response)
+
+
+def _sync(
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ client: httpx.Client,
+ body: UpdateFeedMessageRequestBody,
+) -> FeedMessage:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ body=body,
+ )
+
+ response = client.request(
+ **kwargs,
+ )
+ return _parse_response(response=response)
+
+
+async def _asyncio(
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ client: httpx.AsyncClient,
+ body: UpdateFeedMessageRequestBody,
+) -> FeedMessage:
+ kwargs = _get_kwargs(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ body=body,
+ )
+
+ response = await client.request(
+ **kwargs,
+ )
+
+ return _parse_response(response=response)
diff --git a/packages/liveblocks-python/liveblocks/client.py b/packages/liveblocks-python/liveblocks/client.py
index e92acdfd3cd..6f887e1a3cc 100644
--- a/packages/liveblocks-python/liveblocks/client.py
+++ b/packages/liveblocks-python/liveblocks/client.py
@@ -28,6 +28,8 @@
from .models.create_ai_copilot_options_open_ai import CreateAiCopilotOptionsOpenAi
from .models.create_ai_copilot_options_open_ai_compatible import CreateAiCopilotOptionsOpenAiCompatible
from .models.create_comment_request_body import CreateCommentRequestBody
+ from .models.create_feed_message_request_body import CreateFeedMessageRequestBody
+ from .models.create_feed_request_body import CreateFeedRequestBody
from .models.create_file_knowledge_source_response import CreateFileKnowledgeSourceResponse
from .models.create_group_request_body import CreateGroupRequestBody
from .models.create_room_request_body import CreateRoomRequestBody
@@ -38,7 +40,11 @@
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
+ from .models.feed import Feed
+ from .models.feed_message import FeedMessage
from .models.get_ai_copilots_response import GetAiCopilotsResponse
+ from .models.get_feed_messages_response import GetFeedMessagesResponse
+ from .models.get_feeds_response import GetFeedsResponse
from .models.get_file_knowledge_source_markdown_response import GetFileKnowledgeSourceMarkdownResponse
from .models.get_groups_response import GetGroupsResponse
from .models.get_inbox_notifications_response import GetInboxNotificationsResponse
@@ -83,6 +89,8 @@
from .models.trigger_inbox_notification_request_body import TriggerInboxNotificationRequestBody
from .models.unsubscribe_from_thread_request_body import UnsubscribeFromThreadRequestBody
from .models.update_ai_copilot_request_body import UpdateAiCopilotRequestBody
+ from .models.update_feed_message_request_body import UpdateFeedMessageRequestBody
+ from .models.update_feed_request_body import UpdateFeedRequestBody
from .models.update_notification_settings_request_body import UpdateNotificationSettingsRequestBody
from .models.update_room_id_request_body import UpdateRoomIdRequestBody
from .models.update_room_organization_id_request_body import UpdateRoomOrganizationIdRequestBody
@@ -1726,6 +1734,332 @@ def get_thread_inbox_notifications(
client=self._client,
)
+ def get_feeds(
+ self,
+ room_id: str,
+ *,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+ ) -> GetFeedsResponse:
+ """Get room feeds
+
+ This endpoint returns the feeds in the requested room. Corresponds to
+ [`liveblocks.getFeeds`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ feeds).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ since (int | Unset): Only return feeds with `createdAt` greater than this Unix timestamp
+ in milliseconds. Example: 1660000988137.
+ limit (int | Unset): A limit on the number of feeds to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+
+ 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:
+ GetFeedsResponse
+ """
+
+ from .api.feeds import get_feeds
+
+ return get_feeds._sync(
+ room_id=room_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ client=self._client,
+ )
+
+ def create_feed(
+ self,
+ room_id: str,
+ *,
+ body: CreateFeedRequestBody,
+ ) -> Feed:
+ """Create feed
+
+ This endpoint creates a new feed in a room. Corresponds to
+ [`liveblocks.createFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-feeds).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (CreateFeedRequestBody): Request body for `POST /v2/rooms/{roomId}/feeds`. Optional
+ creation time is sent as `timestamp` (milliseconds), not `createdAt`.
+
+ 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:
+ Feed
+ """
+
+ from .api.feeds import create_feed
+
+ return create_feed._sync(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_feed(
+ self,
+ room_id: str,
+ feed_id: str,
+ ) -> Feed:
+ """Get feed
+
+ This endpoint returns a feed by its ID. Corresponds to
+ [`liveblocks.getFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ feeds-feedId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_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:
+ Feed
+ """
+
+ from .api.feeds import get_feed
+
+ return get_feed._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ client=self._client,
+ )
+
+ def delete_feed(
+ self,
+ room_id: str,
+ feed_id: str,
+ ) -> None:
+ """Delete feed
+
+ This endpoint deletes a feed. Corresponds to
+ [`liveblocks.deleteFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-
+ roomId-feeds-feedId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_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:
+ None
+ """
+
+ from .api.feeds import delete_feed
+
+ return delete_feed._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ client=self._client,
+ )
+
+ def update_feed(
+ self,
+ room_id: str,
+ feed_id: str,
+ *,
+ body: UpdateFeedRequestBody,
+ ) -> Feed:
+ """Update feed
+
+ This endpoint updates the metadata of a feed. Corresponds to
+ [`liveblocks.updateFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-rooms-
+ roomId-feeds-feedId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ body (UpdateFeedRequestBody):
+
+ 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:
+ Feed
+ """
+
+ from .api.feeds import update_feed
+
+ return update_feed._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ client=self._client,
+ )
+
+ def get_feed_messages(
+ self,
+ room_id: str,
+ feed_id: str,
+ *,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+ ) -> GetFeedMessagesResponse:
+ """Get feed messages
+
+ This endpoint returns the messages in a feed. Corresponds to
+ [`liveblocks.getFeedMessages`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomId-feeds-feedId-messages).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ since (int | Unset): Only return messages with `createdAt` greater than this Unix
+ timestamp in milliseconds. Example: 1660000988137.
+ limit (int | Unset): A limit on the number of messages to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+
+ 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:
+ GetFeedMessagesResponse
+ """
+
+ from .api.feeds import get_feed_messages
+
+ return get_feed_messages._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ client=self._client,
+ )
+
+ def create_feed_message(
+ self,
+ room_id: str,
+ feed_id: str,
+ *,
+ body: CreateFeedMessageRequestBody,
+ ) -> FeedMessage:
+ """Create feed message
+
+ This endpoint creates a new message in a feed. Corresponds to
+ [`liveblocks.createFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-feeds-feedId-messages).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ body (CreateFeedMessageRequestBody): Request body for `POST
+ /v2/rooms/{roomId}/feeds/{feedId}/messages`. Optional message time is sent as `timestamp`
+ (milliseconds), not `createdAt`.
+
+ 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:
+ FeedMessage
+ """
+
+ from .api.feeds import create_feed_message
+
+ return create_feed_message._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ client=self._client,
+ )
+
+ def delete_feed_message(
+ self,
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ ) -> None:
+ """Delete feed message
+
+ This endpoint deletes a feed message. Corresponds to
+ [`liveblocks.deleteFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-
+ rooms-roomId-feeds-feedId-messages-messageId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ message_id (str): ID of the message Example: msg_xyz789.
+
+ 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:
+ None
+ """
+
+ from .api.feeds import delete_feed_message
+
+ return delete_feed_message._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ client=self._client,
+ )
+
+ def update_feed_message(
+ self,
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ body: UpdateFeedMessageRequestBody,
+ ) -> FeedMessage:
+ """Update feed message
+
+ This endpoint updates a feed message. Corresponds to
+ [`liveblocks.updateFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-
+ rooms-roomId-feeds-feedId-messages-messageId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ message_id (str): ID of the message Example: msg_xyz789.
+ body (UpdateFeedMessageRequestBody): Request body for `PATCH
+ /v2/rooms/{roomId}/feeds/{feedId}/messages/{messageId}`. Optional update time is sent as
+ `timestamp` (milliseconds), not `updatedAt`.
+
+ 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:
+ FeedMessage
+ """
+
+ from .api.feeds import update_feed_message
+
+ return update_feed_message._sync(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ body=body,
+ client=self._client,
+ )
+
def authorize_user(
self,
*,
@@ -4535,6 +4869,332 @@ async def get_thread_inbox_notifications(
client=self._client,
)
+ async def get_feeds(
+ self,
+ room_id: str,
+ *,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+ ) -> GetFeedsResponse:
+ """Get room feeds
+
+ This endpoint returns the feeds in the requested room. Corresponds to
+ [`liveblocks.getFeeds`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ feeds).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ since (int | Unset): Only return feeds with `createdAt` greater than this Unix timestamp
+ in milliseconds. Example: 1660000988137.
+ limit (int | Unset): A limit on the number of feeds to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+
+ 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:
+ GetFeedsResponse
+ """
+
+ from .api.feeds import get_feeds
+
+ return await get_feeds._asyncio(
+ room_id=room_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ client=self._client,
+ )
+
+ async def create_feed(
+ self,
+ room_id: str,
+ *,
+ body: CreateFeedRequestBody,
+ ) -> Feed:
+ """Create feed
+
+ This endpoint creates a new feed in a room. Corresponds to
+ [`liveblocks.createFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-rooms-
+ roomId-feeds).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ body (CreateFeedRequestBody): Request body for `POST /v2/rooms/{roomId}/feeds`. Optional
+ creation time is sent as `timestamp` (milliseconds), not `createdAt`.
+
+ 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:
+ Feed
+ """
+
+ from .api.feeds import create_feed
+
+ return await create_feed._asyncio(
+ room_id=room_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_feed(
+ self,
+ room_id: str,
+ feed_id: str,
+ ) -> Feed:
+ """Get feed
+
+ This endpoint returns a feed by its ID. Corresponds to
+ [`liveblocks.getFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-roomId-
+ feeds-feedId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_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:
+ Feed
+ """
+
+ from .api.feeds import get_feed
+
+ return await get_feed._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ client=self._client,
+ )
+
+ async def delete_feed(
+ self,
+ room_id: str,
+ feed_id: str,
+ ) -> None:
+ """Delete feed
+
+ This endpoint deletes a feed. Corresponds to
+ [`liveblocks.deleteFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-rooms-
+ roomId-feeds-feedId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_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:
+ None
+ """
+
+ from .api.feeds import delete_feed
+
+ return await delete_feed._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ client=self._client,
+ )
+
+ async def update_feed(
+ self,
+ room_id: str,
+ feed_id: str,
+ *,
+ body: UpdateFeedRequestBody,
+ ) -> Feed:
+ """Update feed
+
+ This endpoint updates the metadata of a feed. Corresponds to
+ [`liveblocks.updateFeed`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-rooms-
+ roomId-feeds-feedId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ body (UpdateFeedRequestBody):
+
+ 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:
+ Feed
+ """
+
+ from .api.feeds import update_feed
+
+ return await update_feed._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def get_feed_messages(
+ self,
+ room_id: str,
+ feed_id: str,
+ *,
+ cursor: str | Unset = UNSET,
+ since: int | Unset = UNSET,
+ limit: int | Unset = 20,
+ ) -> GetFeedMessagesResponse:
+ """Get feed messages
+
+ This endpoint returns the messages in a feed. Corresponds to
+ [`liveblocks.getFeedMessages`](https://liveblocks.io/docs/api-reference/liveblocks-node#get-rooms-
+ roomId-feeds-feedId-messages).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ cursor (str | Unset): A cursor used for pagination. Get the value from the `nextCursor`
+ response of the previous page. Example: eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9.
+ since (int | Unset): Only return messages with `createdAt` greater than this Unix
+ timestamp in milliseconds. Example: 1660000988137.
+ limit (int | Unset): A limit on the number of messages to be returned. The limit can range
+ between 1 and 100, and defaults to 20. Default: 20. Example: 20.
+
+ 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:
+ GetFeedMessagesResponse
+ """
+
+ from .api.feeds import get_feed_messages
+
+ return await get_feed_messages._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ cursor=cursor,
+ since=since,
+ limit=limit,
+ client=self._client,
+ )
+
+ async def create_feed_message(
+ self,
+ room_id: str,
+ feed_id: str,
+ *,
+ body: CreateFeedMessageRequestBody,
+ ) -> FeedMessage:
+ """Create feed message
+
+ This endpoint creates a new message in a feed. Corresponds to
+ [`liveblocks.createFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#post-
+ rooms-roomId-feeds-feedId-messages).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ body (CreateFeedMessageRequestBody): Request body for `POST
+ /v2/rooms/{roomId}/feeds/{feedId}/messages`. Optional message time is sent as `timestamp`
+ (milliseconds), not `createdAt`.
+
+ 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:
+ FeedMessage
+ """
+
+ from .api.feeds import create_feed_message
+
+ return await create_feed_message._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ body=body,
+ client=self._client,
+ )
+
+ async def delete_feed_message(
+ self,
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ ) -> None:
+ """Delete feed message
+
+ This endpoint deletes a feed message. Corresponds to
+ [`liveblocks.deleteFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#delete-
+ rooms-roomId-feeds-feedId-messages-messageId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ message_id (str): ID of the message Example: msg_xyz789.
+
+ 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:
+ None
+ """
+
+ from .api.feeds import delete_feed_message
+
+ return await delete_feed_message._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ client=self._client,
+ )
+
+ async def update_feed_message(
+ self,
+ room_id: str,
+ feed_id: str,
+ message_id: str,
+ *,
+ body: UpdateFeedMessageRequestBody,
+ ) -> FeedMessage:
+ """Update feed message
+
+ This endpoint updates a feed message. Corresponds to
+ [`liveblocks.updateFeedMessage`](https://liveblocks.io/docs/api-reference/liveblocks-node#patch-
+ rooms-roomId-feeds-feedId-messages-messageId).
+
+ Args:
+ room_id (str): ID of the room Example: my-room-id.
+ feed_id (str): ID of the feed Example: fd_abc123.
+ message_id (str): ID of the message Example: msg_xyz789.
+ body (UpdateFeedMessageRequestBody): Request body for `PATCH
+ /v2/rooms/{roomId}/feeds/{feedId}/messages/{messageId}`. Optional update time is sent as
+ `timestamp` (milliseconds), not `updatedAt`.
+
+ 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:
+ FeedMessage
+ """
+
+ from .api.feeds import update_feed_message
+
+ return await update_feed_message._asyncio(
+ room_id=room_id,
+ feed_id=feed_id,
+ message_id=message_id,
+ body=body,
+ client=self._client,
+ )
+
async def authorize_user(
self,
*,
diff --git a/packages/liveblocks-python/liveblocks/models/__init__.py b/packages/liveblocks-python/liveblocks/models/__init__.py
index ecb273ea048..2ca0b43ac33 100644
--- a/packages/liveblocks-python/liveblocks/models/__init__.py
+++ b/packages/liveblocks-python/liveblocks/models/__init__.py
@@ -43,6 +43,10 @@
from .create_ai_copilot_options_open_ai import CreateAiCopilotOptionsOpenAi
from .create_ai_copilot_options_open_ai_compatible import CreateAiCopilotOptionsOpenAiCompatible
from .create_comment_request_body import CreateCommentRequestBody
+from .create_feed_message_request_body import CreateFeedMessageRequestBody
+from .create_feed_message_request_body_data import CreateFeedMessageRequestBodyData
+from .create_feed_request_body import CreateFeedRequestBody
+from .create_feed_request_body_metadata import CreateFeedRequestBodyMetadata
from .create_file_knowledge_source_response import CreateFileKnowledgeSourceResponse
from .create_group_request_body import CreateGroupRequestBody
from .create_group_request_body_scopes import CreateGroupRequestBodyScopes
@@ -61,7 +65,13 @@
from .edit_thread_metadata_request_body import EditThreadMetadataRequestBody
from .edit_thread_metadata_request_body_metadata import EditThreadMetadataRequestBodyMetadata
from .error import Error
+from .feed import Feed
+from .feed_message import FeedMessage
+from .feed_message_data import FeedMessageData
+from .feed_metadata import FeedMetadata
from .get_ai_copilots_response import GetAiCopilotsResponse
+from .get_feed_messages_response import GetFeedMessagesResponse
+from .get_feeds_response import GetFeedsResponse
from .get_file_knowledge_source_markdown_response import GetFileKnowledgeSourceMarkdownResponse
from .get_groups_response import GetGroupsResponse
from .get_inbox_notifications_response import GetInboxNotificationsResponse
@@ -140,6 +150,10 @@
from .unsubscribe_from_thread_request_body import UnsubscribeFromThreadRequestBody
from .update_ai_copilot_request_body import UpdateAiCopilotRequestBody
from .update_ai_copilot_request_body_provider import UpdateAiCopilotRequestBodyProvider
+from .update_feed_message_request_body import UpdateFeedMessageRequestBody
+from .update_feed_message_request_body_data import UpdateFeedMessageRequestBodyData
+from .update_feed_request_body import UpdateFeedRequestBody
+from .update_feed_request_body_metadata import UpdateFeedRequestBodyMetadata
from .update_notification_settings_request_body import UpdateNotificationSettingsRequestBody
from .update_room_id_request_body import UpdateRoomIdRequestBody
from .update_room_organization_id_request_body import UpdateRoomOrganizationIdRequestBody
@@ -207,6 +221,10 @@
"CreateAiCopilotOptionsOpenAi",
"CreateAiCopilotOptionsOpenAiCompatible",
"CreateCommentRequestBody",
+ "CreateFeedMessageRequestBody",
+ "CreateFeedMessageRequestBodyData",
+ "CreateFeedRequestBody",
+ "CreateFeedRequestBodyMetadata",
"CreateFileKnowledgeSourceResponse",
"CreateGroupRequestBody",
"CreateGroupRequestBodyScopes",
@@ -225,7 +243,13 @@
"EditThreadMetadataRequestBody",
"EditThreadMetadataRequestBodyMetadata",
"Error",
+ "Feed",
+ "FeedMessage",
+ "FeedMessageData",
+ "FeedMetadata",
"GetAiCopilotsResponse",
+ "GetFeedMessagesResponse",
+ "GetFeedsResponse",
"GetFileKnowledgeSourceMarkdownResponse",
"GetGroupsResponse",
"GetInboxNotificationsResponse",
@@ -304,6 +328,10 @@
"UnsubscribeFromThreadRequestBody",
"UpdateAiCopilotRequestBody",
"UpdateAiCopilotRequestBodyProvider",
+ "UpdateFeedMessageRequestBody",
+ "UpdateFeedMessageRequestBodyData",
+ "UpdateFeedRequestBody",
+ "UpdateFeedRequestBodyMetadata",
"UpdateNotificationSettingsRequestBody",
"UpdateRoomIdRequestBody",
"UpdateRoomOrganizationIdRequestBody",
diff --git a/packages/liveblocks-python/liveblocks/models/create_feed_message_request_body.py b/packages/liveblocks-python/liveblocks/models/create_feed_message_request_body.py
new file mode 100644
index 00000000000..04db1d0f9c7
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_feed_message_request_body.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+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 ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.create_feed_message_request_body_data import CreateFeedMessageRequestBodyData
+
+
+@_attrs_define
+class CreateFeedMessageRequestBody:
+ """Request body for `POST /v2/rooms/{roomId}/feeds/{feedId}/messages`. Optional message time is sent as `timestamp`
+ (milliseconds), not `createdAt`.
+
+ Attributes:
+ data (CreateFeedMessageRequestBodyData):
+ id (str | Unset): Optional client-provided message id. If omitted, the server generates one.
+ timestamp (float | Unset): Optional. Unix timestamp in milliseconds for the message's creation time. If omitted,
+ the server uses the current time.
+ """
+
+ data: CreateFeedMessageRequestBodyData
+ id: str | Unset = UNSET
+ timestamp: float | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ data = self.data.to_dict()
+
+ id = self.id
+
+ timestamp = self.timestamp
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "data": data,
+ }
+ )
+ if id is not UNSET:
+ field_dict["id"] = id
+ if timestamp is not UNSET:
+ field_dict["timestamp"] = timestamp
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_feed_message_request_body_data import CreateFeedMessageRequestBodyData
+
+ d = dict(src_dict)
+ data = CreateFeedMessageRequestBodyData.from_dict(d.pop("data"))
+
+ id = d.pop("id", UNSET)
+
+ timestamp = d.pop("timestamp", UNSET)
+
+ create_feed_message_request_body = cls(
+ data=data,
+ id=id,
+ timestamp=timestamp,
+ )
+
+ create_feed_message_request_body.additional_properties = d
+ return create_feed_message_request_body
+
+ @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/create_feed_message_request_body_data.py b/packages/liveblocks-python/liveblocks/models/create_feed_message_request_body_data.py
new file mode 100644
index 00000000000..13d2c71bf3f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_feed_message_request_body_data.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class CreateFeedMessageRequestBodyData:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ create_feed_message_request_body_data = cls()
+
+ create_feed_message_request_body_data.additional_properties = d
+ return create_feed_message_request_body_data
+
+ @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/create_feed_request_body.py b/packages/liveblocks-python/liveblocks/models/create_feed_request_body.py
new file mode 100644
index 00000000000..2387494c46f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_feed_request_body.py
@@ -0,0 +1,94 @@
+from __future__ import annotations
+
+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 ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.create_feed_request_body_metadata import CreateFeedRequestBodyMetadata
+
+
+@_attrs_define
+class CreateFeedRequestBody:
+ """Request body for `POST /v2/rooms/{roomId}/feeds`. Optional creation time is sent as `timestamp` (milliseconds), not
+ `createdAt`.
+
+ Attributes:
+ feed_id (str):
+ metadata (CreateFeedRequestBodyMetadata | Unset):
+ timestamp (float | Unset): Optional. Unix timestamp in milliseconds for the feed's creation time. If omitted,
+ the server uses the current time.
+ """
+
+ feed_id: str
+ metadata: CreateFeedRequestBodyMetadata | Unset = UNSET
+ timestamp: float | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ feed_id = self.feed_id
+
+ metadata: dict[str, Any] | Unset = UNSET
+ if not isinstance(self.metadata, Unset):
+ metadata = self.metadata.to_dict()
+
+ timestamp = self.timestamp
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "feedId": feed_id,
+ }
+ )
+ if metadata is not UNSET:
+ field_dict["metadata"] = metadata
+ if timestamp is not UNSET:
+ field_dict["timestamp"] = timestamp
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.create_feed_request_body_metadata import CreateFeedRequestBodyMetadata
+
+ d = dict(src_dict)
+ feed_id = d.pop("feedId")
+
+ _metadata = d.pop("metadata", UNSET)
+ metadata: CreateFeedRequestBodyMetadata | Unset
+ if isinstance(_metadata, Unset):
+ metadata = UNSET
+ else:
+ metadata = CreateFeedRequestBodyMetadata.from_dict(_metadata)
+
+ timestamp = d.pop("timestamp", UNSET)
+
+ create_feed_request_body = cls(
+ feed_id=feed_id,
+ metadata=metadata,
+ timestamp=timestamp,
+ )
+
+ create_feed_request_body.additional_properties = d
+ return create_feed_request_body
+
+ @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/create_feed_request_body_metadata.py b/packages/liveblocks-python/liveblocks/models/create_feed_request_body_metadata.py
new file mode 100644
index 00000000000..04d09a7d7fa
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/create_feed_request_body_metadata.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class CreateFeedRequestBodyMetadata:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ create_feed_request_body_metadata = cls()
+
+ create_feed_request_body_metadata.additional_properties = d
+ return create_feed_request_body_metadata
+
+ @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/feed.py b/packages/liveblocks-python/liveblocks/models/feed.py
new file mode 100644
index 00000000000..96098b14bb4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/feed.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+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
+
+if TYPE_CHECKING:
+ from ..models.feed_metadata import FeedMetadata
+
+
+@_attrs_define
+class Feed:
+ """Feed objects returned by the API use `createdAt` and `updatedAt` (Unix time in milliseconds).
+
+ Attributes:
+ feed_id (str):
+ metadata (FeedMetadata):
+ created_at (float): Unix timestamp in milliseconds when the feed was created.
+ updated_at (float): Unix timestamp in milliseconds when the feed was last updated.
+ """
+
+ feed_id: str
+ metadata: FeedMetadata
+ created_at: float
+ updated_at: float
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ feed_id = self.feed_id
+
+ metadata = self.metadata.to_dict()
+
+ created_at = self.created_at
+
+ updated_at = self.updated_at
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "feedId": feed_id,
+ "metadata": metadata,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.feed_metadata import FeedMetadata
+
+ d = dict(src_dict)
+ feed_id = d.pop("feedId")
+
+ metadata = FeedMetadata.from_dict(d.pop("metadata"))
+
+ created_at = d.pop("createdAt")
+
+ updated_at = d.pop("updatedAt")
+
+ feed = cls(
+ feed_id=feed_id,
+ metadata=metadata,
+ created_at=created_at,
+ updated_at=updated_at,
+ )
+
+ feed.additional_properties = d
+ return feed
+
+ @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/feed_message.py b/packages/liveblocks-python/liveblocks/models/feed_message.py
new file mode 100644
index 00000000000..30f2e24d00f
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/feed_message.py
@@ -0,0 +1,90 @@
+from __future__ import annotations
+
+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
+
+if TYPE_CHECKING:
+ from ..models.feed_message_data import FeedMessageData
+
+
+@_attrs_define
+class FeedMessage:
+ """Message objects returned by the API use `createdAt` and `updatedAt` (Unix time in milliseconds). Request bodies for
+ create/update use `timestamp` for optional custom times.
+
+ Attributes:
+ id (str):
+ created_at (float): Unix timestamp in milliseconds when the message was created.
+ updated_at (float): Unix timestamp in milliseconds when the message was last updated.
+ data (FeedMessageData):
+ """
+
+ id: str
+ created_at: float
+ updated_at: float
+ data: FeedMessageData
+ 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
+
+ updated_at = self.updated_at
+
+ data = self.data.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "id": id,
+ "createdAt": created_at,
+ "updatedAt": updated_at,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.feed_message_data import FeedMessageData
+
+ d = dict(src_dict)
+ id = d.pop("id")
+
+ created_at = d.pop("createdAt")
+
+ updated_at = d.pop("updatedAt")
+
+ data = FeedMessageData.from_dict(d.pop("data"))
+
+ feed_message = cls(
+ id=id,
+ created_at=created_at,
+ updated_at=updated_at,
+ data=data,
+ )
+
+ feed_message.additional_properties = d
+ return feed_message
+
+ @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/feed_message_data.py b/packages/liveblocks-python/liveblocks/models/feed_message_data.py
new file mode 100644
index 00000000000..15f01240084
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/feed_message_data.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class FeedMessageData:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ feed_message_data = cls()
+
+ feed_message_data.additional_properties = d
+ return feed_message_data
+
+ @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/feed_metadata.py b/packages/liveblocks-python/liveblocks/models/feed_metadata.py
new file mode 100644
index 00000000000..584de6cac2e
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/feed_metadata.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class FeedMetadata:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ feed_metadata = cls()
+
+ feed_metadata.additional_properties = d
+ return feed_metadata
+
+ @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/get_feed_messages_response.py b/packages/liveblocks-python/liveblocks/models/get_feed_messages_response.py
new file mode 100644
index 00000000000..0f625d4b274
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_feed_messages_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.feed_message import FeedMessage
+
+
+@_attrs_define
+class GetFeedMessagesResponse:
+ """
+ Example:
+ {'nextCursor': 'eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9', 'data': [{'id': 'msg_xyz789', 'data': {'type': 'text',
+ 'content': 'Hello, world!'}, 'createdAt': 1660000988137, 'updatedAt': 1660000988137}]}
+
+ Attributes:
+ next_cursor (None | str): Pass as `cursor` to fetch the next page, or null when there are no more results.
+ data (list[FeedMessage]):
+ """
+
+ next_cursor: None | str
+ data: list[FeedMessage]
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.feed_message import FeedMessage
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = FeedMessage.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_feed_messages_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_feed_messages_response
diff --git a/packages/liveblocks-python/liveblocks/models/get_feeds_response.py b/packages/liveblocks-python/liveblocks/models/get_feeds_response.py
new file mode 100644
index 00000000000..e6faff17b24
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/get_feeds_response.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Any, Self, cast
+
+from attrs import define as _attrs_define
+
+if TYPE_CHECKING:
+ from ..models.feed import Feed
+
+
+@_attrs_define
+class GetFeedsResponse:
+ """
+ Example:
+ {'nextCursor': 'eyJjcmVhdGVkQXQiOjE2NjAwMDA5ODgxMzd9', 'data': [{'feedId': 'my-feed-id', 'metadata': {'type':
+ 'chat', 'name': 'General Discussion'}, 'createdAt': 1660000988137, 'updatedAt': 1660000988137}]}
+
+ Attributes:
+ next_cursor (None | str): Pass as `cursor` to fetch the next page, or null when there are no more results.
+ data (list[Feed]):
+ """
+
+ next_cursor: None | str
+ data: list[Feed]
+
+ def to_dict(self) -> dict[str, Any]:
+ next_cursor: None | str
+ next_cursor = self.next_cursor
+
+ data = []
+ for data_item_data in self.data:
+ data_item = data_item_data.to_dict()
+ data.append(data_item)
+
+ field_dict: dict[str, Any] = {}
+
+ field_dict.update(
+ {
+ "nextCursor": next_cursor,
+ "data": data,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.feed import Feed
+
+ d = dict(src_dict)
+
+ def _parse_next_cursor(data: object) -> None | str:
+ if data is None:
+ return data
+ return cast(None | str, data)
+
+ next_cursor = _parse_next_cursor(d.pop("nextCursor"))
+
+ data = []
+ _data = d.pop("data")
+ for data_item_data in _data:
+ data_item = Feed.from_dict(data_item_data)
+
+ data.append(data_item)
+
+ get_feeds_response = cls(
+ next_cursor=next_cursor,
+ data=data,
+ )
+
+ return get_feeds_response
diff --git a/packages/liveblocks-python/liveblocks/models/update_feed_message_request_body.py b/packages/liveblocks-python/liveblocks/models/update_feed_message_request_body.py
new file mode 100644
index 00000000000..8ba6fde477c
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_feed_message_request_body.py
@@ -0,0 +1,78 @@
+from __future__ import annotations
+
+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 ..types import UNSET, Unset
+
+if TYPE_CHECKING:
+ from ..models.update_feed_message_request_body_data import UpdateFeedMessageRequestBodyData
+
+
+@_attrs_define
+class UpdateFeedMessageRequestBody:
+ """Request body for `PATCH /v2/rooms/{roomId}/feeds/{feedId}/messages/{messageId}`. Optional update time is sent as
+ `timestamp` (milliseconds), not `updatedAt`.
+
+ Attributes:
+ data (UpdateFeedMessageRequestBodyData):
+ timestamp (float | Unset): Optional. Unix timestamp in milliseconds to record as the update time. If omitted,
+ the server uses the current time.
+ """
+
+ data: UpdateFeedMessageRequestBodyData
+ timestamp: float | Unset = UNSET
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ data = self.data.to_dict()
+
+ timestamp = self.timestamp
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "data": data,
+ }
+ )
+ if timestamp is not UNSET:
+ field_dict["timestamp"] = timestamp
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.update_feed_message_request_body_data import UpdateFeedMessageRequestBodyData
+
+ d = dict(src_dict)
+ data = UpdateFeedMessageRequestBodyData.from_dict(d.pop("data"))
+
+ timestamp = d.pop("timestamp", UNSET)
+
+ update_feed_message_request_body = cls(
+ data=data,
+ timestamp=timestamp,
+ )
+
+ update_feed_message_request_body.additional_properties = d
+ return update_feed_message_request_body
+
+ @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/update_feed_message_request_body_data.py b/packages/liveblocks-python/liveblocks/models/update_feed_message_request_body_data.py
new file mode 100644
index 00000000000..754e6b6dae4
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_feed_message_request_body_data.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class UpdateFeedMessageRequestBodyData:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ update_feed_message_request_body_data = cls()
+
+ update_feed_message_request_body_data.additional_properties = d
+ return update_feed_message_request_body_data
+
+ @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/update_feed_request_body.py b/packages/liveblocks-python/liveblocks/models/update_feed_request_body.py
new file mode 100644
index 00000000000..3bd326ac120
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_feed_request_body.py
@@ -0,0 +1,64 @@
+from __future__ import annotations
+
+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
+
+if TYPE_CHECKING:
+ from ..models.update_feed_request_body_metadata import UpdateFeedRequestBodyMetadata
+
+
+@_attrs_define
+class UpdateFeedRequestBody:
+ """
+ Attributes:
+ metadata (UpdateFeedRequestBodyMetadata):
+ """
+
+ metadata: UpdateFeedRequestBodyMetadata
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+ metadata = self.metadata.to_dict()
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+ field_dict.update(
+ {
+ "metadata": metadata,
+ }
+ )
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ from ..models.update_feed_request_body_metadata import UpdateFeedRequestBodyMetadata
+
+ d = dict(src_dict)
+ metadata = UpdateFeedRequestBodyMetadata.from_dict(d.pop("metadata"))
+
+ update_feed_request_body = cls(
+ metadata=metadata,
+ )
+
+ update_feed_request_body.additional_properties = d
+ return update_feed_request_body
+
+ @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/update_feed_request_body_metadata.py b/packages/liveblocks-python/liveblocks/models/update_feed_request_body_metadata.py
new file mode 100644
index 00000000000..62e3c795589
--- /dev/null
+++ b/packages/liveblocks-python/liveblocks/models/update_feed_request_body_metadata.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any, Self
+
+from attrs import define as _attrs_define
+from attrs import field as _attrs_field
+
+
+@_attrs_define
+class UpdateFeedRequestBodyMetadata:
+ """ """
+
+ additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
+
+ def to_dict(self) -> dict[str, Any]:
+
+ field_dict: dict[str, Any] = {}
+ field_dict.update(self.additional_properties)
+
+ return field_dict
+
+ @classmethod
+ def from_dict(cls, src_dict: Mapping[str, Any]) -> Self:
+ d = dict(src_dict)
+ update_feed_request_body_metadata = cls()
+
+ update_feed_request_body_metadata.additional_properties = d
+ return update_feed_request_body_metadata
+
+ @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/pyproject.toml b/packages/liveblocks-python/pyproject.toml
index 04c821ebbda..5de36bb790c 100644
--- a/packages/liveblocks-python/pyproject.toml
+++ b/packages/liveblocks-python/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "liveblocks"
-version = "3.15.5"
+version = "3.16.0"
description = "A client library for accessing Liveblocks API"
authors = []
requires-python = ">=3.11"
diff --git a/packages/liveblocks-react-blocknote/package.json b/packages/liveblocks-react-blocknote/package.json
index 836d34cb8d0..ceb29e94c8f 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.15.5",
+ "version": "3.16.0",
"description": "An integration of BlockNote + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
@@ -44,12 +44,12 @@
"test:watch": "NODE_OPTIONS=\"--no-deprecation\" vitest"
},
"dependencies": {
- "@liveblocks/client": "3.15.5",
- "@liveblocks/core": "3.15.5",
- "@liveblocks/react": "3.15.5",
- "@liveblocks/react-tiptap": "3.15.5",
- "@liveblocks/react-ui": "3.15.5",
- "@liveblocks/yjs": "3.15.5",
+ "@liveblocks/client": "3.16.0",
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/react": "3.16.0",
+ "@liveblocks/react-tiptap": "3.16.0",
+ "@liveblocks/react-ui": "3.16.0",
+ "@liveblocks/yjs": "3.16.0",
"@tiptap/core": "^3.19.0",
"vitest-tsconfig-paths": "^3.4.1"
},
diff --git a/packages/liveblocks-react-lexical/package.json b/packages/liveblocks-react-lexical/package.json
index 81bc0a29d0d..a057341b43c 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.15.5",
+ "version": "3.16.0",
"description": "An integration of Lexical + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
@@ -45,11 +45,11 @@
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.0",
- "@liveblocks/client": "3.15.5",
- "@liveblocks/core": "3.15.5",
- "@liveblocks/react": "3.15.5",
- "@liveblocks/react-ui": "3.15.5",
- "@liveblocks/yjs": "3.15.5",
+ "@liveblocks/client": "3.16.0",
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/react": "3.16.0",
+ "@liveblocks/react-ui": "3.16.0",
+ "@liveblocks/yjs": "3.16.0",
"radix-ui": "^1.4.0",
"yjs": "^13.6.18"
},
diff --git a/packages/liveblocks-react-tiptap/package.json b/packages/liveblocks-react-tiptap/package.json
index 3e18a8bc1bc..449ef925cc4 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.15.5",
+ "version": "3.16.0",
"description": "An integration of TipTap + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
@@ -45,11 +45,11 @@
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.0",
- "@liveblocks/client": "3.15.5",
- "@liveblocks/core": "3.15.5",
- "@liveblocks/react": "3.15.5",
- "@liveblocks/react-ui": "3.15.5",
- "@liveblocks/yjs": "3.15.5",
+ "@liveblocks/client": "3.16.0",
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/react": "3.16.0",
+ "@liveblocks/react-ui": "3.16.0",
+ "@liveblocks/yjs": "3.16.0",
"@tiptap/core": "^3.19.0",
"@tiptap/react": "^3.19.0",
"@tiptap/suggestion": "^3.19.0",
diff --git a/packages/liveblocks-react-ui/package.json b/packages/liveblocks-react-ui/package.json
index fab33654956..c5d283fddca 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.15.5",
+ "version": "3.16.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.",
@@ -78,9 +78,9 @@
},
"dependencies": {
"@floating-ui/react-dom": "^2.1.0",
- "@liveblocks/client": "3.15.5",
- "@liveblocks/core": "3.15.5",
- "@liveblocks/react": "3.15.5",
+ "@liveblocks/client": "3.16.0",
+ "@liveblocks/core": "3.16.0",
+ "@liveblocks/react": "3.16.0",
"frimousse": "^0.2.0",
"marked": "^15.0.11",
"radix-ui": "^1.4.0",
diff --git a/packages/liveblocks-react-ui/src/components/Comment.tsx b/packages/liveblocks-react-ui/src/components/Comment.tsx
index 1b3c0be89e9..fac1910af75 100644
--- a/packages/liveblocks-react-ui/src/components/Comment.tsx
+++ b/packages/liveblocks-react-ui/src/components/Comment.tsx
@@ -153,6 +153,13 @@ export interface CommentProps
*/
additionalContent?: ReactNode;
+ /**
+ * Override the comment's body.
+ */
+ body?:
+ | ReactNode
+ | ((props: PropsWithChildren<{ comment: CommentData }>) => ReactNode);
+
/**
* The event handler called when the comment is edited.
*/
@@ -688,6 +695,7 @@ export const Comment = Object.assign(
overrides,
components,
additionalContent,
+ body,
avatar,
author,
date,
@@ -912,23 +920,31 @@ export const Comment = Object.assign(
/>
);
} else {
+ const defaultBody = (
+ (
+ onMentionClick?.(mention, event)}
+ overrides={overrides}
+ />
+ ),
+ Link: CommentLink,
+ }}
+ />
+ );
+
content = comment.body ? (
<>
- (
- onMentionClick?.(mention, event)}
- overrides={overrides}
- />
- ),
- Link: CommentLink,
- }}
- />
+ {body === undefined
+ ? defaultBody
+ : typeof body === "function"
+ ? body({ comment, children: defaultBody })
+ : body}
{additionalContent}
{showAttachments &&
(mediaAttachments.length > 0 || fileAttachments.length > 0) ? (
diff --git a/packages/liveblocks-react-ui/src/styles/index.css b/packages/liveblocks-react-ui/src/styles/index.css
index eb2fedb6674..9facde62867 100644
--- a/packages/liveblocks-react-ui/src/styles/index.css
+++ b/packages/liveblocks-react-ui/src/styles/index.css
@@ -1788,6 +1788,10 @@
black calc(var(--lb-avatar-stack-mask-size) + 0.375px)
);
}
+
+ &:where(:last-child) {
+ margin-inline-end: 0;
+ }
}
/*************************************
diff --git a/packages/liveblocks-react/package.json b/packages/liveblocks-react/package.json
index a106e01236e..adddc5e2a39 100644
--- a/packages/liveblocks-react/package.json
+++ b/packages/liveblocks-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react",
- "version": "3.15.5",
+ "version": "3.16.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.",
@@ -63,8 +63,8 @@
"showdeps": "depcruise src --include-only '^src' --exclude='__tests__' --output-type dot | dot -T svg > /tmp/dependency-graph.svg && open /tmp/dependency-graph.svg"
},
"dependencies": {
- "@liveblocks/client": "3.15.5",
- "@liveblocks/core": "3.15.5"
+ "@liveblocks/client": "3.16.0",
+ "@liveblocks/core": "3.16.0"
},
"peerDependencies": {
"@types/react": "*",
diff --git a/packages/liveblocks-react/scripts/check-exports.ts b/packages/liveblocks-react/scripts/check-exports.ts
index 47c0aa3d47f..3cba834f85a 100755
--- a/packages/liveblocks-react/scripts/check-exports.ts
+++ b/packages/liveblocks-react/scripts/check-exports.ts
@@ -22,6 +22,8 @@ const ALLOW_DIFFERENT_JSDOCS = [
"useRoomInfo",
"useSelf",
"useThreads",
+ "useFeeds",
+ "useFeedMessages",
"useUnreadInboxNotificationsCount",
"useUser",
"useGroupInfo",
diff --git a/packages/liveblocks-react/src/__tests__/PaginatedResource.test.ts b/packages/liveblocks-react/src/__tests__/PaginatedResource.test.ts
index 92db799b706..ceeefb3026c 100644
--- a/packages/liveblocks-react/src/__tests__/PaginatedResource.test.ts
+++ b/packages/liveblocks-react/src/__tests__/PaginatedResource.test.ts
@@ -277,4 +277,30 @@ describe("PaginatedResource", () => {
jest.useRealTimers();
}
});
+
+ test("autoRetry: false — single attempt, error persists (no 5s reset)", async () => {
+ const fetcher = jest
+ .fn, [cursor?: string]>()
+ .mockImplementation(() => {
+ throw new Error("permanent");
+ });
+
+ const p = new PaginatedResource(fetcher, { autoRetry: false });
+
+ jest.useFakeTimers();
+ try {
+ const w$ = p.waitUntilLoaded();
+ await expect(w$).rejects.toThrow("permanent");
+ expect(fetcher).toHaveBeenCalledTimes(1);
+
+ await jest.advanceTimersByTimeAsync(5_000);
+ expect(fetcher).toHaveBeenCalledTimes(1);
+ expect(p.get()).toEqual({
+ isLoading: false,
+ error: expect.objectContaining({ message: "permanent" }),
+ });
+ } finally {
+ jest.useRealTimers();
+ }
+ });
});
diff --git a/packages/liveblocks-react/src/contexts.ts b/packages/liveblocks-react/src/contexts.ts
index 420501e11f0..b4a80d2d1dc 100644
--- a/packages/liveblocks-react/src/contexts.ts
+++ b/packages/liveblocks-react/src/contexts.ts
@@ -7,7 +7,7 @@ import type {
LsonObject,
Room,
} from "@liveblocks/client";
-import type { OpaqueClient, OpaqueRoom } from "@liveblocks/core";
+import type { DFM, DFMD, OpaqueClient, OpaqueRoom } from "@liveblocks/core";
import { raise } from "@liveblocks/core";
import { type Context, createContext, useContext } from "react";
@@ -55,10 +55,12 @@ export function useRoomOrNull<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = DFM,
+ FMD extends Json = DFMD,
>(
RoomContext: Context = GlobalRoomContext
-): Room | null {
- return useContext(RoomContext) as Room
| null;
+): Room
| null {
+ return useContext(RoomContext) as Room
| null;
}
/**
diff --git a/packages/liveblocks-react/src/index.ts b/packages/liveblocks-react/src/index.ts
index 19d51082376..c6d664e57e2 100644
--- a/packages/liveblocks-react/src/index.ts
+++ b/packages/liveblocks-react/src/index.ts
@@ -87,6 +87,14 @@ export {
useSelf,
useStorage,
useThreads,
+ useFeeds,
+ useFeedMessages,
+ useCreateFeed,
+ useDeleteFeed,
+ useUpdateFeedMetadata,
+ useCreateFeedMessage,
+ useDeleteFeedMessage,
+ useUpdateFeedMessage,
useSearchComments,
useAttachmentUrl,
useHistoryVersions,
diff --git a/packages/liveblocks-react/src/lib/querying.ts b/packages/liveblocks-react/src/lib/querying.ts
index d1ceb42978e..00d8d471267 100644
--- a/packages/liveblocks-react/src/lib/querying.ts
+++ b/packages/liveblocks-react/src/lib/querying.ts
@@ -4,6 +4,8 @@ import type {
ThreadData,
} from "@liveblocks/client";
import {
+ type Feed,
+ type FeedFetchMetadataFilter,
getSubscriptionKey,
isNumberOperator,
isStartsWithOperator,
@@ -107,6 +109,35 @@ function matchesNumberOperator(
);
}
+/**
+ * Creates a predicate function that will filter Feed instances matching the
+ * given options. `metadata` is matched by exact equality per key. `since`
+ * keeps feeds whose `updatedAt` or `createdAt` is >= the given timestamp.
+ */
+export function makeFeedsFilter(options?: {
+ metadata?: FeedFetchMetadataFilter;
+ since?: number;
+}): (feed: Feed) => boolean {
+ return (feed: Feed) => {
+ if (
+ options?.since !== undefined &&
+ feed.updatedAt < options.since &&
+ feed.createdAt < options.since
+ ) {
+ return false;
+ }
+ if (
+ options?.metadata !== undefined &&
+ !Object.entries(options.metadata).every(
+ ([k, v]) => (feed.metadata as Record)[k] === v
+ )
+ ) {
+ return false;
+ }
+ return true;
+ };
+}
+
export function makeInboxNotificationsFilter(
query: InboxNotificationsQuery
): (inboxNotification: InboxNotificationData) => boolean {
diff --git a/packages/liveblocks-react/src/room.tsx b/packages/liveblocks-react/src/room.tsx
index a59892128f7..76826a6cba1 100644
--- a/packages/liveblocks-react/src/room.tsx
+++ b/packages/liveblocks-react/src/room.tsx
@@ -4,6 +4,8 @@ import type {
BroadcastOptions,
Client,
CommentData,
+ FeedCreateMetadata,
+ FeedUpdateMetadata,
History,
Json,
JsonObject,
@@ -22,11 +24,14 @@ import type {
CommentsEventServerMsg,
DCM,
DE,
+ DFM,
+ DFMD,
DP,
DS,
DTM,
DU,
EnterOptions,
+ FeedsEventServerMsg,
IYjsProvider,
LiveblocksErrorContext,
MentionData,
@@ -90,6 +95,10 @@ import type {
EditCommentMetadataOptions,
EditCommentOptions,
EditThreadMetadataOptions,
+ FeedMessagesAsyncResult,
+ FeedMessagesAsyncSuccess,
+ FeedsAsyncResult,
+ FeedsAsyncSuccess,
HistoryVersionDataAsyncResult,
HistoryVersionsAsyncResult,
HistoryVersionsAsyncSuccess,
@@ -103,16 +112,22 @@ import type {
ThreadsAsyncResult,
ThreadsAsyncSuccess,
ThreadSubscription,
+ UseFeedMessagesOptions,
+ UseFeedsOptions,
UseSearchCommentsOptions,
UseThreadsOptions,
} from "./types";
import type { UmbrellaStore } from "./umbrella-store";
-import { makeRoomThreadsQueryKey } from "./umbrella-store";
+import {
+ makeFeedMessagesQueryKey,
+ makeFeedsQueryKey,
+ makeRoomThreadsQueryKey,
+} from "./umbrella-store";
import { useScrollToCommentOnLoadEffect } from "./use-scroll-to-comment-on-load-effect";
import { useSignal } from "./use-signal";
import { useSyncExternalStoreWithSelector } from "./use-sync-external-store-with-selector";
-const noop = () => {};
+const noop = () => { };
const identity: (x: T) => T = (x) => x;
const STABLE_EMPTY_LIST = Object.freeze([]);
@@ -318,8 +333,10 @@ type RoomLeavePair<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = Json,
+ FMD extends Json = Json,
> = {
- room: Room;
+ room: Room
;
leave: () => void;
};
@@ -330,6 +347,8 @@ function RoomProvider<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = Json,
+ FMD extends Json = Json,
>(
props: RoomProviderProps
& {
/** @internal */
@@ -338,21 +357,21 @@ function RoomProvider<
) {
const client = useClient();
const [cache] = useState(
- () => new Map>()
+ () => new Map>()
);
// Produce a version of client.enterRoom() that when called for the same
// room ID multiple times, will not keep producing multiple leave
// functions, but instead return the cached one.
- const stableEnterRoom: typeof client.enterRoom = useCallback(
+ const stableEnterRoom: typeof client.enterRoom
= useCallback(
(
roomId: string,
options: EnterOptions
- ): RoomLeavePair
=> {
+ ): RoomLeavePair
=> {
const cached = cache.get(roomId);
if (cached) return cached;
- const rv = client.enterRoom
(roomId, options);
+ const rv = client.enterRoom
(roomId, options);
// Wrap the leave function to also delete the cached value
const origLeave = rv.leave;
@@ -387,7 +406,7 @@ function RoomProvider<
// Room to not be freed and destroyed when the component unmounts later.
//
return (
-
+
{...(props as any)}
stableEnterRoom={stableEnterRoom}
/>
@@ -401,10 +420,12 @@ type EnterRoomType<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = Json,
+ FMD extends Json = Json,
> = (
roomId: string,
options: EnterOptions
-) => RoomLeavePair
;
+) => RoomLeavePair
;
/** @internal */
function RoomProviderInner<
@@ -414,9 +435,11 @@ function RoomProviderInner<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = Json,
+ FMD extends Json = Json,
>(
props: RoomProviderProps
& {
- stableEnterRoom: EnterRoomType
;
+ stableEnterRoom: EnterRoomType
;
BoundRoomContext?: Context;
}
) {
@@ -525,6 +548,36 @@ function RoomProviderInner<
);
}, [client, room]);
+ useEffect(() => {
+ const { store } = getRoomExtrasForClient(client);
+
+ function handleFeedEvent(message: FeedsEventServerMsg): void {
+ switch (message.type) {
+ case ServerMsgCode.FEEDS_ADDED:
+ case ServerMsgCode.FEEDS_UPDATED:
+ store.upsertFeeds(room.id, message.feeds);
+ break;
+ case ServerMsgCode.FEED_DELETED:
+ store.deleteFeed(room.id, message.feedId);
+ break;
+ case ServerMsgCode.FEED_MESSAGES_ADDED:
+ case ServerMsgCode.FEED_MESSAGES_UPDATED:
+ store.upsertFeedMessages(room.id, message.feedId, message.messages);
+ break;
+ case ServerMsgCode.FEED_MESSAGES_DELETED:
+ store.deleteFeedMessages(room.id, message.feedId, message.messageIds);
+ break;
+ // FEEDS_LIST and FEED_MESSAGES_LIST are handled by fetch promise resolution in room.ts
+ default:
+ break;
+ }
+ }
+
+ return room.events.feeds.subscribe(
+ (message: FeedsEventServerMsg) => void handleFeedEvent(message)
+ );
+ }, [client, room]);
+
useEffect(() => {
const pair = stableEnterRoom(roomId, frozenProps);
@@ -569,10 +622,12 @@ function useRoom_withRoomContext<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
+ FM extends Json = Json,
+ FMD extends Json = Json,
>(
RoomContext: Context,
options?: { allowOutsideRoom: false }
-): Room;
+): Room
;
function useRoom_withRoomContext<
P extends JsonObject = DP,
S extends LsonObject = DS,
@@ -580,10 +635,12 @@ function useRoom_withRoomContext<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
+ FM extends Json = Json,
+ FMD extends Json = Json,
>(
RoomContext: Context,
options?: { allowOutsideRoom: boolean }
-): Room | null;
+): Room
| null;
function useRoom_withRoomContext<
P extends JsonObject = DP,
S extends LsonObject = DS,
@@ -591,11 +648,13 @@ function useRoom_withRoomContext<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
+ FM extends Json = Json,
+ FMD extends Json = Json,
>(
RoomContext: Context,
options?: { allowOutsideRoom: boolean }
-): Room | null {
- const room = useRoomOrNull
(RoomContext);
+): Room
| null {
+ const room = useRoomOrNull
(RoomContext);
if (room === null && !options?.allowOutsideRoom) {
throw new Error("RoomProvider is missing from the React tree.");
@@ -611,7 +670,9 @@ function useRoom<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
->(options?: { allowOutsideRoom: false }): Room
;
+ FM extends Json = Json,
+ FMD extends Json = Json,
+>(options?: { allowOutsideRoom: false }): Room
;
function useRoom<
P extends JsonObject = DP,
S extends LsonObject = DS,
@@ -619,7 +680,9 @@ function useRoom<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
->(options: { allowOutsideRoom: boolean }): Room
| null;
+ FM extends Json = Json,
+ FMD extends Json = Json,
+>(options: { allowOutsideRoom: boolean }): Room
| null;
function useRoom<
P extends JsonObject = DP,
S extends LsonObject = DS,
@@ -627,8 +690,10 @@ function useRoom<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
->(options?: { allowOutsideRoom: boolean }): Room
| null {
- return useRoom_withRoomContext
(
+ FM extends Json = Json,
+ FMD extends Json = Json,
+>(options?: { allowOutsideRoom: boolean }): Room
| null {
+ return useRoom_withRoomContext
(
GlobalRoomContext,
options
);
@@ -1470,6 +1535,265 @@ function useThreads_withRoomContext<
return result;
}
+function useFeeds_withRoomContext(
+ RoomContext: Context,
+ options?: UseFeedsOptions
+): FeedsAsyncResult {
+ const room = useRoom_withRoomContext(RoomContext);
+ const client = useClient();
+ const { store } = getRoomExtrasForClient(client);
+ const queryKey = makeFeedsQueryKey(room.id, options);
+
+ const loadableResource = store.outputs.loadingFeeds.getOrCreate(queryKey);
+
+ useEffect(() => {
+ void loadableResource.waitUntilLoaded();
+ }, [room, loadableResource]);
+
+ return useSignal(loadableResource.signal);
+}
+
+function useFeeds(options?: UseFeedsOptions): FeedsAsyncResult {
+ return useFeeds_withRoomContext(GlobalRoomContext, options);
+}
+
+function useFeedMessages_withRoomContext(
+ RoomContext: Context,
+ feedId: string,
+ options?: UseFeedMessagesOptions
+): FeedMessagesAsyncResult {
+ const room = useRoom_withRoomContext(RoomContext);
+ const client = useClient();
+ const { store } = getRoomExtrasForClient(client);
+ const queryKey = makeFeedMessagesQueryKey(room.id, feedId, options);
+
+ useEffect(() => {
+ void store.outputs.loadingFeedMessages
+ .getOrCreate(queryKey)
+ .waitUntilLoaded();
+ });
+
+ return useSignal(
+ store.outputs.loadingFeedMessages.getOrCreate(queryKey).signal
+ );
+}
+
+function useFeedMessages(
+ feedId: string,
+ options?: UseFeedMessagesOptions
+): FeedMessagesAsyncResult {
+ return useFeedMessages_withRoomContext(GlobalRoomContext, feedId, options);
+}
+
+function useFeedsSuspense_withRoomContext(
+ RoomContext: Context,
+ options?: UseFeedsOptions
+): FeedsAsyncSuccess {
+ ensureNotServerSide();
+ const client = useClient();
+ const room = useRoom_withRoomContext(RoomContext);
+
+ const { store } = getRoomExtrasForClient(client);
+ const queryKey = makeFeedsQueryKey(room.id, options);
+
+ use(store.outputs.loadingFeeds.getOrCreate(queryKey).waitUntilLoaded());
+
+ const result = useFeeds_withRoomContext(RoomContext, options);
+ assert(!result.error, "Did not expect error");
+ assert(!result.isLoading, "Did not expect loading");
+ return result as FeedsAsyncSuccess;
+}
+
+function useFeedsSuspense(options?: UseFeedsOptions): FeedsAsyncSuccess {
+ return useFeedsSuspense_withRoomContext(GlobalRoomContext, options);
+}
+
+function useFeedMessagesSuspense_withRoomContext(
+ RoomContext: Context,
+ feedId: string,
+ options?: UseFeedMessagesOptions
+): FeedMessagesAsyncSuccess {
+ ensureNotServerSide();
+
+ const client = useClient();
+ const room = useRoom_withRoomContext(RoomContext);
+
+ const { store } = getRoomExtrasForClient(client);
+ const queryKey = makeFeedMessagesQueryKey(room.id, feedId, options);
+
+ use(store.outputs.loadingFeedMessages.getOrCreate(queryKey).waitUntilLoaded());
+
+ const result = useFeedMessages_withRoomContext(RoomContext, feedId, options);
+ assert(!result.error, "Did not expect error");
+ assert(!result.isLoading, "Did not expect loading");
+ return result as FeedMessagesAsyncSuccess;
+}
+
+function useFeedMessagesSuspense(
+ feedId: string,
+ options?: UseFeedMessagesOptions
+): FeedMessagesAsyncSuccess {
+ return useFeedMessagesSuspense_withRoomContext(
+ GlobalRoomContext,
+ feedId,
+ options
+ );
+}
+
+function useCreateFeed_withRoomContext(
+ RoomContext: Context
+): (
+ feedId: string,
+ options?: { metadata?: FeedCreateMetadata; createdAt?: number }
+) => Promise {
+ const room = useRoom_withRoomContext(RoomContext);
+ return useCallback(
+ (feedId, options) => room.addFeed(feedId, options),
+ [room]
+ );
+}
+
+/**
+ * Returns a function that creates a new feed in the current room.
+ *
+ * @example
+ * const createFeed = useCreateFeed();
+ * createFeed("feed-id", { metadata: { name: "My Feed" } });
+ */
+function useCreateFeed(): (
+ feedId: string,
+ options?: { metadata?: FeedCreateMetadata; createdAt?: number }
+) => Promise {
+ return useCreateFeed_withRoomContext(GlobalRoomContext);
+}
+
+function useDeleteFeed_withRoomContext(
+ RoomContext: Context
+): (feedId: string) => Promise {
+ const room = useRoom_withRoomContext(RoomContext);
+ return useCallback((feedId) => room.deleteFeed(feedId), [room]);
+}
+
+/**
+ * Returns a function that deletes a feed from the current room.
+ *
+ * @example
+ * const deleteFeed = useDeleteFeed();
+ * deleteFeed("feed-id");
+ */
+function useDeleteFeed(): (feedId: string) => Promise {
+ return useDeleteFeed_withRoomContext(GlobalRoomContext);
+}
+
+function useUpdateFeedMetadata_withRoomContext(
+ RoomContext: Context
+): (feedId: string, metadata: FeedUpdateMetadata) => Promise {
+ const room = useRoom_withRoomContext(RoomContext);
+ return useCallback(
+ (feedId, metadata) => room.updateFeed(feedId, metadata),
+ [room]
+ );
+}
+
+/**
+ * Returns a function that updates a feed's metadata in the current room.
+ *
+ * @example
+ * const updateFeedMetadata = useUpdateFeedMetadata();
+ * updateFeedMetadata("feed-id", { name: "Updated Name" });
+ */
+function useUpdateFeedMetadata(): (
+ feedId: string,
+ metadata: FeedUpdateMetadata
+) => Promise {
+ return useUpdateFeedMetadata_withRoomContext(GlobalRoomContext);
+}
+
+function useCreateFeedMessage_withRoomContext(
+ RoomContext: Context
+): (
+ feedId: string,
+ data: JsonObject,
+ options?: { id?: string; createdAt?: number }
+) => Promise {
+ const room = useRoom_withRoomContext(RoomContext);
+ return useCallback(
+ (feedId, data, options) => room.addFeedMessage(feedId, data, options),
+ [room]
+ );
+}
+
+/**
+ * Returns a function that adds a message to a feed in the current room.
+ *
+ * @example
+ * const createFeedMessage = useCreateFeedMessage();
+ * createFeedMessage("feed-id", { text: "Hello" });
+ */
+function useCreateFeedMessage(): (
+ feedId: string,
+ data: JsonObject,
+ options?: { id?: string; createdAt?: number }
+) => Promise {
+ return useCreateFeedMessage_withRoomContext(GlobalRoomContext);
+}
+
+function useDeleteFeedMessage_withRoomContext(
+ RoomContext: Context
+): (feedId: string, messageId: string) => Promise {
+ const room = useRoom_withRoomContext(RoomContext);
+ return useCallback(
+ (feedId, messageId) => room.deleteFeedMessage(feedId, messageId),
+ [room]
+ );
+}
+
+/**
+ * Returns a function that deletes a message from a feed in the current room.
+ *
+ * @example
+ * const deleteFeedMessage = useDeleteFeedMessage();
+ * deleteFeedMessage("feed-id", "message-id");
+ */
+function useDeleteFeedMessage(): (
+ feedId: string,
+ messageId: string
+) => Promise {
+ return useDeleteFeedMessage_withRoomContext(GlobalRoomContext);
+}
+
+function useUpdateFeedMessage_withRoomContext(
+ RoomContext: Context
+): (
+ feedId: string,
+ messageId: string,
+ data: JsonObject,
+ options?: { updatedAt?: number }
+) => Promise {
+ const room = useRoom_withRoomContext(RoomContext);
+ return useCallback(
+ (feedId, messageId, data, options) =>
+ room.updateFeedMessage(feedId, messageId, data, options),
+ [room]
+ );
+}
+
+/**
+ * Returns a function that updates a feed message in the current room.
+ *
+ * @example
+ * const updateFeedMessage = useUpdateFeedMessage();
+ * updateFeedMessage("feed-id", "message-id", { text: "Updated" });
+ */
+function useUpdateFeedMessage(): (
+ feedId: string,
+ messageId: string,
+ data: JsonObject,
+ options?: { updatedAt?: number }
+) => Promise {
+ return useUpdateFeedMessage_withRoomContext(GlobalRoomContext);
+}
+
function useThreads(
options: UseThreadsOptions = {}
): ThreadsAsyncResult {
@@ -2005,9 +2329,9 @@ function useEditRoomComment(
const updatedMetadata =
metadata !== undefined
? {
- ...comment.metadata,
- ...metadata,
- }
+ ...comment.metadata,
+ ...metadata,
+ }
: comment.metadata;
const optimisticId = store.optimisticUpdates.add({
@@ -2648,9 +2972,9 @@ function useRoomThreadSubscription(
function useRoomSubscriptionSettings_withRoomContext(
RoomContext: Context
): [
- RoomSubscriptionSettingsAsyncResult,
- (settings: Partial) => void,
-] {
+ RoomSubscriptionSettingsAsyncResult,
+ (settings: Partial) => void,
+ ] {
const updateRoomSubscriptionSettings =
useUpdateRoomSubscriptionSettings_withRoomContext(RoomContext);
const client = useClient();
@@ -2713,9 +3037,9 @@ function useRoomSubscriptionSettings(): [
function useRoomSubscriptionSettingsSuspense_withRoomContext(
RoomContext: Context
): [
- RoomSubscriptionSettingsAsyncSuccess,
- (settings: Partial) => void,
-] {
+ RoomSubscriptionSettingsAsyncSuccess,
+ (settings: Partial) => void,
+ ] {
// Throw error if we're calling this hook server side
ensureNotServerSide();
@@ -2785,8 +3109,8 @@ function useHistoryVersionData_withRoomContext(
error instanceof Error
? error
: new Error(
- "An unknown error occurred while loading this version"
- ),
+ "An unknown error occurred while loading this version"
+ ),
});
}
};
@@ -3380,9 +3704,11 @@ export function createRoomContext<
E extends Json = DE,
TM extends BaseMetadata = DTM,
CM extends BaseMetadata = DCM,
->(client: OpaqueClient): RoomContextBundle {
- type TRoom = Room
;
- type TRoomBundle = RoomContextBundle
;
+ FM extends Json = Json,
+ FMD extends Json = Json,
+>(client: OpaqueClient): RoomContextBundle
{
+ type TRoom = Room
;
+ type TRoomBundle = RoomContextBundle
;
const BoundRoomContext = createContext(null);
@@ -3677,8 +4003,56 @@ export function createRoomContext<
return useUpdateRoomSubscriptionSettings_withRoomContext(BoundRoomContext);
}
+ function useFeeds_withBoundRoomContext(
+ ...args: Parameters
+ ) {
+ return useFeeds_withRoomContext(BoundRoomContext, ...args);
+ }
+
+ function useFeedMessages_withBoundRoomContext(
+ ...args: Parameters
+ ) {
+ return useFeedMessages_withRoomContext(BoundRoomContext, ...args);
+ }
+
+ function useFeedsSuspense_withBoundRoomContext(
+ ...args: Parameters
+ ) {
+ return useFeedsSuspense_withRoomContext(BoundRoomContext, ...args);
+ }
+
+ function useFeedMessagesSuspense_withBoundRoomContext(
+ ...args: Parameters
+ ) {
+ return useFeedMessagesSuspense_withRoomContext(BoundRoomContext, ...args);
+ }
+
+ function useCreateFeed_withBoundRoomContext() {
+ return useCreateFeed_withRoomContext(BoundRoomContext);
+ }
+
+ function useDeleteFeed_withBoundRoomContext() {
+ return useDeleteFeed_withRoomContext(BoundRoomContext);
+ }
+
+ function useUpdateFeedMetadata_withBoundRoomContext() {
+ return useUpdateFeedMetadata_withRoomContext(BoundRoomContext);
+ }
+
+ function useCreateFeedMessage_withBoundRoomContext() {
+ return useCreateFeedMessage_withRoomContext(BoundRoomContext);
+ }
+
+ function useDeleteFeedMessage_withBoundRoomContext() {
+ return useDeleteFeedMessage_withRoomContext(BoundRoomContext);
+ }
+
+ function useUpdateFeedMessage_withBoundRoomContext() {
+ return useUpdateFeedMessage_withRoomContext(BoundRoomContext);
+ }
+
const shared = createSharedContext(client as Client);
- const bundle: RoomContextBundle = {
+ const bundle: RoomContextBundle
= {
RoomContext: BoundRoomContext as Context,
RoomProvider:
RoomProvider_withImplicitLiveblocksProviderAndBoundRoomContext,
@@ -3732,6 +4106,22 @@ export function createRoomContext<
// prettier-ignore
useThreads: useThreads_withBoundRoomContext as TRoomBundle["useThreads"],
// prettier-ignore
+ useFeeds: useFeeds_withBoundRoomContext as TRoomBundle["useFeeds"],
+ // prettier-ignore
+ useFeedMessages: useFeedMessages_withBoundRoomContext as TRoomBundle["useFeedMessages"],
+ // prettier-ignore
+ useCreateFeed: useCreateFeed_withBoundRoomContext as TRoomBundle["useCreateFeed"],
+ // prettier-ignore
+ useDeleteFeed: useDeleteFeed_withBoundRoomContext as TRoomBundle["useDeleteFeed"],
+ // prettier-ignore
+ useUpdateFeedMetadata: useUpdateFeedMetadata_withBoundRoomContext as TRoomBundle["useUpdateFeedMetadata"],
+ // prettier-ignore
+ useCreateFeedMessage: useCreateFeedMessage_withBoundRoomContext as TRoomBundle["useCreateFeedMessage"],
+ // prettier-ignore
+ useDeleteFeedMessage: useDeleteFeedMessage_withBoundRoomContext as TRoomBundle["useDeleteFeedMessage"],
+ // prettier-ignore
+ useUpdateFeedMessage: useUpdateFeedMessage_withBoundRoomContext as TRoomBundle["useUpdateFeedMessage"],
+ // prettier-ignore
useCreateThread: useCreateThread_withBoundRoomContext as TRoomBundle["useCreateThread"],
// prettier-ignore
useDeleteThread: useDeleteThread_withBoundRoomContext as TRoomBundle["useDeleteThread"],
@@ -3832,6 +4222,22 @@ export function createRoomContext<
// prettier-ignore
useThreads: useThreadsSuspense_withBoundRoomContext as TRoomBundle["suspense"]["useThreads"],
// prettier-ignore
+ useFeeds: useFeedsSuspense_withBoundRoomContext as TRoomBundle["suspense"]["useFeeds"],
+ // prettier-ignore
+ useFeedMessages: useFeedMessagesSuspense_withBoundRoomContext as TRoomBundle["suspense"]["useFeedMessages"],
+ // prettier-ignore
+ useCreateFeed: useCreateFeed_withBoundRoomContext as TRoomBundle["suspense"]["useCreateFeed"],
+ // prettier-ignore
+ useDeleteFeed: useDeleteFeed_withBoundRoomContext as TRoomBundle["suspense"]["useDeleteFeed"],
+ // prettier-ignore
+ useUpdateFeedMetadata: useUpdateFeedMetadata_withBoundRoomContext as TRoomBundle["suspense"]["useUpdateFeedMetadata"],
+ // prettier-ignore
+ useCreateFeedMessage: useCreateFeedMessage_withBoundRoomContext as TRoomBundle["suspense"]["useCreateFeedMessage"],
+ // prettier-ignore
+ useDeleteFeedMessage: useDeleteFeedMessage_withBoundRoomContext as TRoomBundle["suspense"]["useDeleteFeedMessage"],
+ // prettier-ignore
+ useUpdateFeedMessage: useUpdateFeedMessage_withBoundRoomContext as TRoomBundle["suspense"]["useUpdateFeedMessage"],
+ // prettier-ignore
useCreateThread: useCreateThread_withBoundRoomContext as TRoomBundle["suspense"]["useCreateThread"],
// prettier-ignore
useDeleteThread: useDeleteThread_withBoundRoomContext as TRoomBundle["suspense"]["useDeleteThread"],
@@ -3884,7 +4290,7 @@ export function createRoomContext<
});
}
-type TypedBundle = RoomContextBundle;
+type TypedBundle = RoomContextBundle;
/**
* Makes a Room available in the component hierarchy below.
@@ -4137,6 +4543,40 @@ const _useOthersMappedSuspense: TypedBundle["suspense"]["useOthersMapped"] =
*/
const _useThreads: TypedBundle["useThreads"] = useThreads;
+/**
+ * Returns feeds for the current room.
+ *
+ * @example
+ * const { feeds, error, isLoading } = useFeeds();
+ */
+const _useFeeds: TypedBundle["useFeeds"] = useFeeds;
+
+/**
+ * Returns messages for a specific feed in the current room.
+ *
+ * @example
+ * const { messages, error, isLoading } = useFeedMessages("feed-id");
+ */
+const _useFeedMessages: TypedBundle["useFeedMessages"] = useFeedMessages;
+
+/**
+ * Returns feeds for the current room.
+ *
+ * @example
+ * const { feeds } = useFeeds();
+ */
+const _useFeedsSuspense: TypedBundle["suspense"]["useFeeds"] =
+ useFeedsSuspense;
+
+/**
+ * Returns messages for a specific feed in the current room.
+ *
+ * @example
+ * const { messages } = useFeedMessages("feed-id");
+ */
+const _useFeedMessagesSuspense: TypedBundle["suspense"]["useFeedMessages"] =
+ useFeedMessagesSuspense;
+
/**
* Returns the result of searching comments by text in the current room. The result includes the id and the plain text content of the matched comments along with the parent thread id of the comment.
*
@@ -4464,11 +4904,15 @@ export {
useCanRedo,
useCanUndo,
_useCreateComment as useCreateComment,
+ useCreateFeed,
+ useCreateFeedMessage,
useCreateRoomComment,
useCreateRoomThread,
useCreateTextMention,
_useCreateThread as useCreateThread,
useDeleteComment,
+ useDeleteFeed,
+ useDeleteFeedMessage,
useDeleteRoomComment,
useDeleteRoomThread,
useDeleteTextMention,
@@ -4480,6 +4924,10 @@ export {
useEditRoomThreadMetadata,
_useEditThreadMetadata as useEditThreadMetadata,
_useEventListener as useEventListener,
+ _useFeedMessages as useFeedMessages,
+ _useFeedMessagesSuspense as useFeedMessagesSuspense,
+ _useFeeds as useFeeds,
+ _useFeedsSuspense as useFeedsSuspense,
useHistory,
useHistoryVersionData,
_useHistoryVersions as useHistoryVersions,
@@ -4530,6 +4978,8 @@ export {
useUndo,
useUnsubscribeFromRoomThread,
useUnsubscribeFromThread,
+ useUpdateFeedMessage,
+ useUpdateFeedMetadata,
_useUpdateMyPresence as useUpdateMyPresence,
useUpdateRoomSubscriptionSettings,
useYjsProvider,
diff --git a/packages/liveblocks-react/src/suspense.ts b/packages/liveblocks-react/src/suspense.ts
index 5f38ff63f99..cced0c0c5dd 100644
--- a/packages/liveblocks-react/src/suspense.ts
+++ b/packages/liveblocks-react/src/suspense.ts
@@ -49,8 +49,12 @@ export {
useCanRedo,
useCanUndo,
useCreateComment,
+ useCreateFeed,
+ useCreateFeedMessage,
useCreateThread,
useDeleteComment,
+ useDeleteFeed,
+ useDeleteFeedMessage,
useDeleteThread,
useEditComment,
useEditThreadMetadata,
@@ -74,6 +78,8 @@ export {
useStorageRoot,
useThreadSubscription,
useUndo,
+ useUpdateFeedMetadata,
+ useUpdateFeedMessage,
useUpdateMyPresence,
useUpdateRoomSubscriptionSettings,
} from "./room";
@@ -91,6 +97,8 @@ export {
useAttachmentUrlSuspense as useAttachmentUrl,
useHistoryVersionsSuspense as useHistoryVersions,
useRoomSubscriptionSettingsSuspense as useRoomSubscriptionSettings,
+ useFeedsSuspense as useFeeds,
+ useFeedMessagesSuspense as useFeedMessages,
} from "./room";
export {
useInboxNotificationsSuspense as useInboxNotifications,
diff --git a/packages/liveblocks-react/src/types/index.ts b/packages/liveblocks-react/src/types/index.ts
index d56ab6bd712..f8229a53fd0 100644
--- a/packages/liveblocks-react/src/types/index.ts
+++ b/packages/liveblocks-react/src/types/index.ts
@@ -29,6 +29,11 @@ import type {
CommentData,
DGI,
DRI,
+ Feed,
+ FeedCreateMetadata,
+ FeedFetchMetadataFilter,
+ FeedMessage,
+ FeedUpdateMetadata,
GroupData,
HistoryVersion,
InboxNotificationData,
@@ -210,6 +215,42 @@ export type UseInboxNotificationsOptions = {
query?: InboxNotificationsQuery;
};
+export type UseFeedsOptions = {
+ /**
+ * Optional timestamp filter. Applied to the client-side cache for this hook’s
+ * options: only feeds whose `createdAt` or `updatedAt` is at or after this
+ * timestamp (ms) are included in `feeds`.
+ */
+ since?: number;
+ /**
+ * Optional metadata filter (`Record`). Applied to the
+ * client-side cache: only feeds whose metadata matches every key/value pair
+ * are included in `feeds`.
+ */
+ metadata?: FeedFetchMetadataFilter;
+ /**
+ * Page size for each server request when loading or loading more feeds. This
+ * does **not** cap the length of `feeds`—use pagination (`fetchMore`,
+ * `hasFetchedAll`) until you have loaded every page. Different hooks with
+ * different `limit` values still share one cache per room; each hook’s
+ * `feeds` array is filtered and sorted independently.
+ */
+ limit?: number;
+};
+
+export type UseFeedMessagesOptions = {
+ /**
+ * Optional cursor for pagination.
+ */
+ cursor?: string;
+ /**
+ * Page size for each server request when loading or loading more messages.
+ * Does **not** cap the length of `messages`—pagination loads additional pages
+ * until `hasFetchedAll` is true.
+ */
+ limit?: number;
+};
+
export type UserAsyncResult = AsyncResult;
export type UserAsyncSuccess = AsyncSuccess;
@@ -296,6 +337,12 @@ export type SearchCommentsAsyncResult = AsyncResult,
export type InboxNotificationsAsyncSuccess = PagedAsyncSuccess; // prettier-ignore
export type InboxNotificationsAsyncResult = PagedAsyncResult; // prettier-ignore
+export type FeedsAsyncSuccess = PagedAsyncSuccess[], "feeds">; // prettier-ignore
+export type FeedsAsyncResult = PagedAsyncResult[], "feeds">; // prettier-ignore
+
+export type FeedMessagesAsyncSuccess = PagedAsyncSuccess[], "messages">; // prettier-ignore
+export type FeedMessagesAsyncResult = PagedAsyncResult[], "messages">; // prettier-ignore
+
export type UnreadInboxNotificationsCountAsyncSuccess = AsyncSuccess; // prettier-ignore
export type UnreadInboxNotificationsCountAsyncResult = AsyncResult; // prettier-ignore
@@ -601,13 +648,15 @@ type RoomContextBundleCommon<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = Json,
+ FMD extends Json = Json,
> = {
/**
* You normally don't need to directly interact with the RoomContext, but
* it can be necessary if you're building an advanced app where you need to
* set up a context bridge between two React renderers.
*/
- RoomContext: Context | null>;
+ RoomContext: Context | null>;
/**
* Makes a Room available in the component hierarchy below.
@@ -620,10 +669,12 @@ type RoomContextBundleCommon<
* Returns the Room of the nearest RoomProvider above in the React component
* tree.
*/
- useRoom(options?: { allowOutsideRoom: false }): Room;
+ useRoom(options?: {
+ allowOutsideRoom: false;
+ }): Room
;
useRoom(options: {
allowOutsideRoom: boolean;
- }): Room
| null;
+ }): Room
| null;
/**
* Returns the current connection status for the Room, and triggers
@@ -1084,8 +1135,10 @@ export type RoomContextBundle<
E extends Json,
TM extends BaseMetadata,
CM extends BaseMetadata,
+ FM extends Json = Json,
+ FMD extends Json = Json,
> = Resolve<
- RoomContextBundleCommon
&
+ RoomContextBundleCommon
&
SharedContextBundle["classic"] & {
/**
* Extract arbitrary data from the Liveblocks Storage state, using an
@@ -1158,6 +1211,97 @@ export type RoomContextBundle<
*/
useThreads(options?: UseThreadsOptions): ThreadsAsyncResult;
+ /**
+ * Returns feeds for the current room.
+ *
+ * @example
+ * const { feeds, error, isLoading } = useFeeds();
+ */
+ useFeeds(options?: UseFeedsOptions): FeedsAsyncResult;
+
+ /**
+ * Returns messages for a specific feed in the current room.
+ *
+ * @example
+ * const { messages, error, isLoading } = useFeedMessages("feed-id");
+ */
+ useFeedMessages(
+ feedId: string,
+ options?: UseFeedMessagesOptions
+ ): FeedMessagesAsyncResult;
+
+ /**
+ * Returns a function that creates a new feed in the current room.
+ *
+ * @example
+ * const createFeed = useCreateFeed();
+ * createFeed("feed-id", { metadata: { name: "My Feed" } });
+ */
+ useCreateFeed(): (
+ feedId: string,
+ options?: { metadata?: FeedCreateMetadata; createdAt?: number }
+ ) => Promise;
+
+ /**
+ * Returns a function that deletes a feed from the current room.
+ *
+ * @example
+ * const deleteFeed = useDeleteFeed();
+ * deleteFeed("feed-id");
+ */
+ useDeleteFeed(): (feedId: string) => Promise;
+
+ /**
+ * Returns a function that updates a feed's metadata in the current room.
+ *
+ * @example
+ * const updateFeedMetadata = useUpdateFeedMetadata();
+ * updateFeedMetadata("feed-id", { name: "Updated Name" });
+ */
+ useUpdateFeedMetadata(): (
+ feedId: string,
+ metadata: FeedUpdateMetadata
+ ) => Promise;
+
+ /**
+ * Returns a function that adds a message to a feed in the current room.
+ *
+ * @example
+ * const createFeedMessage = useCreateFeedMessage();
+ * createFeedMessage("feed-id", { text: "Hello" });
+ */
+ useCreateFeedMessage(): (
+ feedId: string,
+ data: JsonObject,
+ options?: { id?: string; createdAt?: number }
+ ) => Promise;
+
+ /**
+ * Returns a function that deletes a message from a feed in the current room.
+ *
+ * @example
+ * const deleteFeedMessage = useDeleteFeedMessage();
+ * deleteFeedMessage("feed-id", "message-id");
+ */
+ useDeleteFeedMessage(): (
+ feedId: string,
+ messageId: string
+ ) => Promise;
+
+ /**
+ * Returns a function that updates a feed message in the current room.
+ *
+ * @example
+ * const updateFeedMessage = useUpdateFeedMessage();
+ * updateFeedMessage("feed-id", "message-id", { text: "Updated" });
+ */
+ useUpdateFeedMessage(): (
+ feedId: string,
+ messageId: string,
+ data: JsonObject,
+ options?: { updatedAt?: number }
+ ) => Promise