Upgrade to v8
This page lists what changed between v7 and v8, and what to change in your app.
v8 is built on stream-chat (the JavaScript client) v10. Most of the SDK's own API (components, services, templates) is unchanged, but some types and behaviors changed because stream-chat v10 changed its data model. If your app calls the stream-chat client directly (chatService.chatClient, channel.*), the most common client changes are summarized in Using the stream-chat client directly. The complete list is stream-chat v9 to v10 breaking changes in the stream-chat repository.
Quick migration checklist
- Upgrade both packages:
npm install stream-chat-angular@rc stream-chat@rc. See Installation and requirements. - Stream API v2 changes: all response dates are
TimestampNS, and all custom data is sent in thecustomwrapper. See Stream API v2 changes. - Move client options under
clientinChatClientService.init. See Client initialization. - Pass the channel list sort as an array:
[{ field: "last_message_at", direction: -1 }]. See Sort. - Prefix custom fields in channel filters with
custom.. See Channel list. - Read custom data from
custom: a channel's name ischannel.data?.custom?.name. See Custom data is under custom. - Convert message dates with
nsToDate:StreamMessagedates areTimestampNS, notDate. See StreamMessage and other message types. - Switch custom channel queries to
queryChannelsAndHydrate. See Custom channel queries. - Stop reading messages from
channel.state. Use the SDK's observables or the channel's message paginator. See Message list and pagination. - Add
custom: {}to attachments you create, and read attachment metadata fromattachment.custom. See Attachments and voice recordings. - Replace
connection.changedlisteners withconnectionState$. See Connection state. - Check bounced and blocked messages with
message.moderationinstead ofmoderation_details. See Moderation. - Add the new translation key if you provide your own translations. See Translations.
- Update error handling for
StreamAPIError: it replacesErrorFromResponse, and itsmessageno longer has theStreamChat error codeprefix. See Error handling.
Installation and requirements
v8 is a release candidate. Install it, together with stream-chat v10, from the rc tag:
npm install stream-chat-angular@rc stream-chat@rc- The
stream-chatpeer dependency changed from^9.20.0to^10.0.0. stream-chatv10 requires Node.js 22.18.0 or later for installation and builds.- The
stream-chatv10 type definitions work with TypeScript 5.2 or later (the minimum supported by Angular 17). - Angular 17 to 22 are still supported, see Angular version support.
- The CSS (
@stream-io/stream-chat-css) and theming didn't change.
Stream API v2 changes
stream-chat v10 uses v2 of the Stream API. Two changes affect almost every part of your app:
- All response dates are
TimestampNS. Date fields in API responses (and in the state built from them) are unix nanoseconds, a brandednumber, instead ofDateobjects or ISO strings. Convert them with thensToDatehelper exported bystream-chatwhen you need aDate. See StreamMessage and other message types. - All custom data is sent in the
customwrapper. Custom fields of channels, messages, users, attachments, reactions, polls and events are no longer top-level fields: they are sent and returned under the entity'scustomobject. This applies to channel filters as well (custom.prefix). See Custom data is under custom and Filters.
Client initialization
Client options
The last parameter of ChatClientService.init is now the stream-chat v10 configuration tree (DeepPartial<InstanceConfigTree>). Client options moved under the client key, and axios options under client.axiosRequestConfig. trackPendingChannelInvites stays a top-level option.
In v7:
chatService.init(apiKey, userId, token, {
timeout: 10000,
trackPendingChannelInvites: true,
});In v8:
chatService.init(apiKey, userId, token, {
client: { axiosRequestConfig: { timeout: 10000 } },
trackPendingChannelInvites: true,
});The StreamChat options mapping lists where every option moved.
User parameter
The type of the userOrId parameter changed from string | OwnUserResponse | UserResponse to string | ClientUser. ClientUser is exported by stream-chat: it's a partial OwnUserResponse with a required id. You can pass { id, name, image } objects as before. Custom user fields go under custom, see Custom data is under custom.
State recovery after reconnect
The SDK no longer sets recoverStateOnReconnect (the option doesn't exist in stream-chat v10). Connection recovery is on by default. After a reconnect, the SDK refreshes the channel list as before, and stream-chat reloads the active channel and the open thread.
Don't turn connection recovery off (client.connectionRecovery.enabled: false). Then stream-chat doesn't dispatch the connection.recovered event, and the SDK doesn't refresh the channel list after a reconnect.
Connection state
stream-chat v10 no longer emits the connection.changed event. ChatClientService.connectionState$ ('online' | 'offline') and the "Connection failure, reconnecting now..." notification still work, they are now based on the WebSocket's health.
connectionState$ switches to 'offline' only if the connection stays unhealthy for 5 seconds. A shorter outage emits nothing.
If you listened to connection.changed on ChatClientService.events$ or with chatClient.on, use connectionState$ instead (or chatClient.wsConnection.state if you need the raw WebSocket health):
chatService.connectionState$.subscribe((state) => {
console.log(`Chat is ${state}`);
});Channel list
Filters
ChannelFilters is now the stream-chat v10 type. Custom fields need the custom. prefix, and each key allows only one operator (combine more with $and).
In v7:
channelService.init({ type: "messaging", color: "green" });In v8:
channelService.init({ type: "messaging", "custom.color": "green" });Sort
The sort parameter of ChannelService.init (and the ChannelQuery constructor) takes the stream-chat v10 sort format: an array of { field, direction } objects (SortParamRequest[] from stream-chat). The object format ({ last_message_at: -1 }) is no longer accepted, and the ChannelSort type no longer exists.
In v7:
channelService.init(filters, { last_message_at: -1 });In v8:
import { SortParamRequest } from "stream-chat";
const sort: SortParamRequest[] = [{ field: "last_message_at", direction: -1 }];
channelService.init(filters, sort);The order of the array sets the sort priority, the same way the key order of the object format did.
Custom channel queries
In stream-chat v10, client.queryChannels returns the raw API response and doesn't create Channel instances. If your initWithCustomQuery function called queryChannels, switch to queryChannelsAndHydrate. Filter, sort and options move into one request object.
In v7:
channelService.initWithCustomQuery(async () => {
const channels = await client.queryChannels(
filters,
{ last_message_at: -1 },
{ limit: 25 },
);
return { channels, hasMorePage: channels.length === 25 };
});In v8:
channelService.initWithCustomQuery(async () => {
const channels = await client.queryChannelsAndHydrate({
filter_conditions: filters,
sort: [{ field: "last_message_at", direction: -1 }],
limit: 25,
});
return { channels, hasMorePage: channels.length === 25 };
});The Channel list state page explains how the channel list works.
Custom data is under custom
In stream-chat v10, every custom field lives under the custom object of its entity (channel, message, user, attachment, reaction, poll, event). Update the templates and code that read or write custom data.
Channel name and image
name and image are custom channel fields, so they moved too.
In v7:
{{ channel.data?.name }} <img [src]="channel.data?.image" />In v8:
{{ channel.data?.custom?.name }} <img [src]="channel.data?.custom?.image" />The SDK's DefaultChannelData type now types channel.data.custom.name and channel.data.custom.image. If you extend CustomChannelData (see Custom types), your fields are typed under channel.data.custom as well.
Messages
Custom message fields are read from message.custom (for example message.custom.priority). The customData parameter of ChannelService.sendMessage is sent as custom, you don't need to change how you call it. The customData returned by beforeSendMessage works the same way.
Users, reactions and events
- Custom user fields are under
user.custom.nameandimagestay top-level user fields. - The
customDataparameter ofChannelService.addReactionis sent asreaction.custom. - Custom event fields are under
event.custom.
StreamMessage and other message types
StreamMessage is now based on the stream-chat v10 LocalMessage type instead of FormatMessageResponse (removed in v10). The SDK doesn't convert the date fields: like everywhere in stream-chat v10, they are TimestampNS values (unix nanoseconds, a branded number). Convert them with the nsToDate helper exported by stream-chat when you need a Date.
In v7:
const sentAt = message.created_at.toLocaleString();In v8:
import { nsToDate } from "stream-chat";
const sentAt = nsToDate(message.created_at).toLocaleString();Angular's date pipe treats numbers as milliseconds, so convert the date in your component (or with your own pipe) before passing it to the pipe.
| Change | v7 | v8 |
|---|---|---|
| Base type | FormatMessageResponse |
LocalMessage |
Date fields (created_at, updated_at, deleted_at, pinned_at, pin_expires, message_text_updated_at) |
Date (or null) |
TimestampNS (unix nanoseconds), optional fields are undefined, not null |
quoted_message |
MessageResponseBase & { translation } |
MessageResponse & { translation } (dates are TimestampNS) |
| Custom fields | top-level | under custom |
moderation_details |
moderation result (v1) | removed, use moderation (see Moderation) |
__html |
set on message previews | removed |
DateParserService and custom date parsers still receive Date objects: the SDK converts the message dates before calling them. The date separator template context (DateSeparatorContext.date) is also a Date.
If you need the exact object stream-chat stores (for example to call a stream-chat API), get it from the channel's message paginator: channel.messagePaginator.getItem(message.id).
stream-chat no longer exports these message types: FormatMessageResponse (use LocalMessage), MessageResponseBase (use MessageResponse), DraftMessagePayload (use MessageRequest) and CommandResponse (use Command).
Message list and pagination
stream-chat v10 removed channel.state.messages, latestMessages, messageSets, threads, pinnedMessages and loadMessageIntoState. Messages now live in channel.messagePaginator, pinned messages in channel.pinnedMessagesPaginator and thread replies in thread.messagePaginator. The SDK's observables (activeChannelMessages$, activeThreadMessages$, activeChannelPinnedMessages$) work as before, but code that read the channel state directly has to change:
| v7 | v8 |
|---|---|
channelService.activeChannel.state.messages |
channelService.activeChannelMessages$ or channel.messagePaginator.items |
channel.state.latestMessages |
channel.messagePaginator.anchoredHeadItems (latest loaded messages), channel.messagePaginator.latestMessage (latest message) |
channel.state.pinnedMessages |
channelService.activeChannelPinnedMessages$ or channel.pinnedMessagesPaginator.items |
channel.state.threads[parentId] |
channelService.activeThreadMessages$ or client.threads.get(parentId)?.messagePaginator.items |
channel.state.loadMessageIntoState(id) |
channelService.jumpToMessage(id) |
channel.lastMessage() |
channel.messagePaginator.latestMessage |
channel.state.messages = [] / addMessageSorted / removeMessage |
channel.messagePaginator.removeItem({ id }) / ingestItem(message) (v10 LocalMessage) |
loadMoreMessages and loadMoreThreadReplies
- The return type of
loadMoreMessageschanged fromfalse | Promise<QueryChannelAPIResponse>tofalse | Promise<void>.loadMoreThreadRepliesstill returnsfalse | Promise<void>. - Both return
false(no request) if there's nothing more to load in the given direction, a page is already loading, or no message is loaded yet. If the request fails, the returned promise rejects. loadMoreThreadReplies('newer')now loads newer replies (in v7 it always returnedfalse). This happens after jumping to an older reply.
jumpToMessage
The signature of jumpToMessage is unchanged, but it now uses the message paginator of stream-chat v10:
- If the message isn't found or can't be loaded, the promise rejects and the "Message not found" notification is displayed (same as before).
- Jumping to a thread reply also loads the parent message into the channel's message list if needed, then opens the thread and jumps to the reply.
Channel preview
The type of ChannelPreviewComponent's latestMessage changed from FormatMessageResponse to LocalMessage, and the latestMessage field of the channel preview info template context (ChannelPreviewInfoContext) changed from StreamMessage to LocalMessage. Its dates are TimestampNS, and it doesn't have the readBy and translation fields. The latest message now comes from channel.messagePaginator, so custom channel preview templates no longer need message.new, message.updated and message.deleted listeners to stay up to date.
Threads
- In
stream-chatv10, channels don't store thread replies. Replies are stored byThreadinstances held byclient.threads. The SDK creates theThread(client.threads.ensure) when you callsetAsActiveParentMessage. channel.getReplieswas removed in v10, and the SDK no longer calls it. If your code did, useclient.threads.get(parentId)?.messagePaginator, or the rawclient.getReplies({ parent_id }, options)request.- The second parameter of
setAsActiveParentMessage(loadMessagesFrom) is deprecated and ignored: replies are always loaded by the thread. - The SDK calls
client.threads.registerSubscriptions()inChannelService.init, threads need it to receive WebSocket events. Don't callclient.threads.unregisterSubscriptions(), it disposes the reply lists of the threads.
Custom event handlers and setters
The custom event handlers of ChannelService (customNewMessageHandler, customChannelUpdatedHandler, customChannelTruncatedHandler, customChannelDeletedHandler, customChannelHiddenHandler, customChannelVisibleHandler, customNewMessageNotificationHandler, customAddedToChannelNotificationHandler and customRemovedFromChannelNotificationHandler) keep their signatures, with these changes:
-
The
eventparameter (andClientEvent.event) is the v10Eventtype. It's a union of the event types: a field only exists on the events that have it, so narrow byevent.typefirst:if (event.type === "notification.message_new") { const channel = event.channel; }stream-chatrenamedEventTypestoEventType. -
messageListSetterandthreadListSetterstill work, but the message lists are owned by the message paginators ofstream-chat: the next paginator update (for example the next message) overwrites the list you set. To permanently change a message list, update the paginator instead (channel.messagePaginator.removeItem/ingestItem). -
channel.updated: the SDK no longer copiesevent.channelintochannel.data,stream-chatv10 does it (keepinghidden,own_capabilitiesandmember_count). -
channel.truncated: the SDK no longer clearschannel.state.messages,stream-chatv10 truncates the message list. -
The SDK no longer handles
message.updated,message.deletedandreaction.*events itself: the message lists are updated from the message paginators.
Attachments and voice recordings
In stream-chat v10, attachment metadata moved under attachment.custom. This includes the fields added by the SDK:
| v7 | v8 |
|---|---|
attachment.mime_type |
attachment.custom.mime_type |
attachment.file_size |
attachment.custom.file_size |
attachment.duration |
attachment.custom.duration |
attachment.waveform_data |
attachment.custom.waveform_data |
attachment.img_url (deprecated) |
attachment.custom.img_url (deprecated) |
attachment.isCustomAttachment |
attachment.custom.isCustomAttachment |
-
Every attachment needs a
customobject (it can be empty). Addcustom: {}to the attachments you create, for example withAttachmentService.addAttachment:attachmentService.addAttachment({ type: "image", image_url: "https://example.com/image.png", custom: {}, }); -
AttachmentUpload.extraDatais merged into the attachment, so put custom metadata inextraData.custom. -
GalleryAttachment(the gallery context of custom attachment templates) has a new, requiredcustomproperty. -
DefaultAttachmentDatahas two new fields:durationandwaveform_data. -
If you have custom attachment templates, read the fields from their new location.
-
Custom upload and delete handlers (
customImageUploadRequest,customFileUploadRequest,customImageDeleteRequest,customFileDeleteRequest) are unchanged. The default upload requests now usechannel.uploadImage/uploadFile(channel.sendImage/sendFilewere removed in v10). -
AttachmentListComponentcompares attachment URLs without their query string. Attachment URLs are signed, and some message updates (for example adding a reaction) renew the signature, somessage.attachmentscan change even if the attachments are the same.AttachmentListComponentignores these changes, so images and videos aren't reloaded. If you display attachments with your own component instead ofstream-attachment-list, you may want to do the same.
Message input and drafts
MessageInputComponent.messageDraftChangeemitsMessageRequest | undefined(it wasDraftMessagePayload | undefined, removed in v10). The fields are the same (text,attachments,mentioned_users,poll_id,parent_id,quoted_message_id).MessageInputComponent.loadDrafttakes the v10DraftResponse:draft.message.mentioned_usersis now a list of users (it was a list of ids), and dates areTimestampNS. To save a draft withstream-chatv10, callchannel.createDraft({ message: draft }).- Slash commands come from
channel.config.availableCommands(it waschannel.getConfig().commands).ComandAutocompleteListItemis now based onCommand(it wasCommandResponse).
Polls
-
stream-chatv10 renamed the poll types, and the SDK's poll components use the new names. This matters for custom poll templates and if you use the poll components directly:v7 v8 PollOptionPollOptionResponseDataPollVote,PollAnswerPollVoteResponseDataAffected inputs:
PollVoteComponent.vote,UpsertAnswerComponent.answer,PollVoteResultsListComponent.optionandPollOptionSelectorComponent.option. -
VotingVisibilityis now a string union type ('anonymous' | 'public'), not an enum. -
Poll, option, vote and answer dates are
TimestampNS(unix nanoseconds), convert them withnsToDate. -
Sort options of poll queries use the array format, for example
[{ field: 'created_at', direction: -1 }].
Reactions
MessageReactionsComponent.messageReactionGroupsuses the v10ReactionGroupResponse:first_reaction_atandlast_reaction_atareTimestampNS.- Custom reaction fields are under
reaction.custom. ChannelService.getMessageReactions(deprecated) andMessageReactionsService.queryReactionswork as before.
Moderation
stream-chat v10 messages no longer have moderation_details, use message.moderation instead:
- Bounced messages:
message.type === 'error' && message.moderation?.action === 'bounce'. - Blocked messages:
message.type === 'error' && message.moderation?.action === 'remove'.
If your custom message template or MessageBouncePromptComponent customization checked moderation_details, switch to moderation.
Other type changes
| Type / API | v7 | v8 |
|---|---|---|
ChatClientService.appSettings$ / AppSettings |
AppSettings from stream-chat |
AppSettings from stream-chat-angular (Partial<AppResponseFields>) |
ChannelService.activeChannelMessages getter |
(StreamMessage | MessageResponse | FormatMessageResponse)[] |
(StreamMessage | MessageResponse | LocalMessage)[] |
ChannelService.selectMessageToQuote |
StreamMessage | MessageResponse | undefined |
also accepts LocalMessage |
MessageTextContext.message |
StreamMessage | MessageResponseBase | undefined |
StreamMessage | MessageResponse | undefined |
ChannelSort |
from stream-chat |
removed, use SortParamRequest[] from stream-chat |
getChannelDisplayText(channel, user) |
user: UserResponse |
user?: { id?: string } |
listUsers(users) |
users: UserResponse[] |
users: { id: string; name?: string }[] |
AvatarComponent.user, AvatarPlaceholderComponent.user, AvatarContext.user |
User |
UserResponse |
getReadBy(message, channel) |
message: FormatMessageResponse |
message: Pick<MessageResponse, 'created_at' | 'user'> |
createMessagePreview(...) |
returns MessageResponse |
returns LocalMessage (customData is set as custom) |
MessageListComponent.parseDate |
parseDate(date: Date) |
parseDate(timestamp: TimestampNS) |
Translations
There is a new translation key: streamChat.Error loading messages. It's displayed if the messages of a channel or thread can't be loaded. If you provide your own translations (see Translation), add this key.
Error handling
When an API request fails, stream-chat v10 throws a StreamAPIError (exported by stream-chat). It replaces the ErrorFromResponse class of v9, which no longer exists: update your imports and instanceof checks.
| Field | v9 (ErrorFromResponse) |
v10 (StreamAPIError) |
|---|---|---|
message |
StreamChat error code <code>: <server message> |
the server's error message, without the prefix |
code |
Stream error code (null if there is none) |
Stream error code (undefined if there is none) |
status |
HTTP status code | HTTP status code |
response |
the HTTP response | the HTTP response |
name |
ErrorFromResponse |
StreamAPIError(status: <status>, code: <code>) |
Read the fields from the error object itself. The error's toJSON() only keeps message, stack, name, code and status, so JSON.stringify(error) (or JSON.parse(JSON.stringify(error))) drops response. If your code serializes errors to read response.data, switch to reading the error directly.
Errors that don't come from the Stream API (for example network errors, timeouts or HTTP errors without a Stream error body) are thrown as they are, so check the type before reading Stream specific fields:
import { StreamAPIError } from "stream-chat";
try {
await channel.markUnread({ message_id: messageId });
} catch (error) {
if (error instanceof StreamAPIError) {
// HTTP status, Stream error code, server error message, raw HTTP response
console.log(error.status, error.code, error.message, error.response?.data);
} else {
// network error, timeout, etc.
}
}The SDK's own error notifications and the errorStatusCode of failed messages (the HTTP status code) work with these errors, no change is needed there.
Behavior changes
These changes don't require code changes, but your app may behave differently:
- Reconnect: after a reconnect, the SDK refreshes the channel list (same as before), and
stream-chatreloads the active channel and the open thread. The open thread now stays open (in v7 it was closed because its replies weren't reloaded). - Message list updates: messages arriving via WebSocket can be displayed up to 500 ms later, because
stream-chatv10 batches message list updates. Messages sent by the current user are displayed immediately. - Message objects: when a message changes (for example a reaction is added), only that message gets a new object in
activeChannelMessages$. - Hard-deleted user messages:
user.messages.deletedevents withhard_deleteremove the messages instead of displaying a "This message was deleted" placeholder. - Read events: the active channel is marked as read with
channel.markReadViaReporter(). If read events are disabled for the channel, the SDK doesn't send read requests, and marking a message as unread fails with an error notification. - Typing events are only sent if typing events are enabled for the channel (
channel.config.typingEvents.enabled).
New APIs
ChannelService.activeChannelMessagesPaginationState$andactiveThreadMessagesPaginationState$: the pagination state of the message lists (MessageListPaginationState:hasMoreOlder,hasMoreNewer,isLoading,error).ChannelService.jumpToFirstUnreadMessage(): jumps to the first unread message of the active channel. It returnsfalseif there is no unread message to jump to. If loading the message fails, it displays the "Message not found" notification and throws the error.ChannelService.reloadActiveChannelMessages(): retries loading the messages of the active channel, for example after an error.getTranslationLanguage(channel, user): the language messages are translated to (the user'slanguage, or the channel'sauto_translation_language). The SDK updates message translations when either of them changes.- New exported types:
MessageListPaginationStateandAppSettings.
Using the stream-chat client directly
If you use chatService.chatClient or Channel objects directly, these are the stream-chat v10 changes you're most likely to meet. The stream-chat v10 breaking changes list all of them.
| stream-chat v9 | stream-chat v10 |
|---|---|
client.activeChannels[cid] |
client.channelManager.get(cid) |
client.queryChannels(filters, sort, options) |
client.queryChannelsAndHydrate({ filter_conditions, sort: [{ field, direction }], ...options }) |
client.queryUsers(filters, sort, options) |
client.queryUsers({ payload: { filter_conditions, sort, ...options } }) |
client.flagMessage(id) |
client.moderation.flagMessage({ message_id: id }) |
client.updateMessage(message) |
client.updateMessage({ id }, { message }) |
client.deleteMessage(id, hard) |
client.deleteMessage({ id }, { hard }) |
client.pinMessage(message) / unpinMessage(message) |
client.pinMessage({ id }) / unpinMessage({ id }) |
client.queryReactions(id, filter, sort, options) |
client.queryReactions({ id }, { filter, sort, ...options }) |
client.on('connection.changed', ...) |
client.wsConnection.state.subscribeWithSelector(({ isHealthy }) => ({ isHealthy }), ...) |
client.logger(...) |
chatLoggerSystem |
channel.sendMessage(message) |
channel.sendMessage({ message }) |
channel.sendReaction(id, reaction) / deleteReaction(id, type) |
channel.sendReaction({ id }, { reaction }) / deleteReaction({ id, type }) |
channel.markRead() |
channel.markReadViaReporter() (markRead() is now the raw request) |
channel.getConfig() |
channel.serverConfig (server config), channel.config (resolved config) |
channel.sendImage / sendFile |
channel.uploadImage({ file }) / uploadFile({ file }) |
channel.queryMembers(filters, sort, options) |
channel.queryMembers({ payload: { filter_conditions, sort, ...options } }) |
channel.getReplies(parentId, options) |
client.threads.get(parentId)?.messagePaginator, or client.getReplies({ parent_id }, options) |
channel.state.* message storage |
channel.messagePaginator (see Message list and pagination) |
Date fields as string / Date |
TimestampNS (unix nanoseconds), convert with nsToDate / dateToNs |
| Custom fields at the top level | under custom |
Sort as { field: direction } |
[{ field, direction }] |