Info:
This is release candidate documentation for Stream Chat Angular SDK v8. For the latest stable version, see the latest version (v7).

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

  1. Upgrade both packages: npm install stream-chat-angular@rc stream-chat@rc. See Installation and requirements.
  2. Stream API v2 changes: all response dates are TimestampNS, and all custom data is sent in the custom wrapper. See Stream API v2 changes.
  3. Move client options under client in ChatClientService.init. See Client initialization.
  4. Pass the channel list sort as an array: [{ field: "last_message_at", direction: -1 }]. See Sort.
  5. Prefix custom fields in channel filters with custom.. See Channel list.
  6. Read custom data from custom: a channel's name is channel.data?.custom?.name. See Custom data is under custom.
  7. Convert message dates with nsToDate: StreamMessage dates are TimestampNS, not Date. See StreamMessage and other message types.
  8. Switch custom channel queries to queryChannelsAndHydrate. See Custom channel queries.
  9. Stop reading messages from channel.state. Use the SDK's observables or the channel's message paginator. See Message list and pagination.
  10. Add custom: {} to attachments you create, and read attachment metadata from attachment.custom. See Attachments and voice recordings.
  11. Replace connection.changed listeners with connectionState$. See Connection state.
  12. Check bounced and blocked messages with message.moderation instead of moderation_details. See Moderation.
  13. Add the new translation key if you provide your own translations. See Translations.
  14. Update error handling for StreamAPIError: it replaces ErrorFromResponse, and its message no longer has the StreamChat error code prefix. 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-chat peer dependency changed from ^9.20.0 to ^10.0.0.
  • stream-chat v10 requires Node.js 22.18.0 or later for installation and builds.
  • The stream-chat v10 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 branded number, instead of Date objects or ISO strings. Convert them with the nsToDate helper exported by stream-chat when you need a Date. See StreamMessage and other message types.
  • All custom data is sent in the custom wrapper. 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's custom object. 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. name and image stay top-level user fields.
  • The customData parameter of ChannelService.addReaction is sent as reaction.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 loadMoreMessages changed from false | Promise<QueryChannelAPIResponse> to false | Promise<void>. loadMoreThreadReplies still returns false | 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 returned false). 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-chat v10, channels don't store thread replies. Replies are stored by Thread instances held by client.threads. The SDK creates the Thread (client.threads.ensure) when you call setAsActiveParentMessage.
  • channel.getReplies was removed in v10, and the SDK no longer calls it. If your code did, use client.threads.get(parentId)?.messagePaginator, or the raw client.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() in ChannelService.init, threads need it to receive WebSocket events. Don't call client.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 event parameter (and ClientEvent.event) is the v10 Event type. It's a union of the event types: a field only exists on the events that have it, so narrow by event.type first:

    if (event.type === "notification.message_new") {
      const channel = event.channel;
    }

    stream-chat renamed EventTypes to EventType.

  • messageListSetter and threadListSetter still work, but the message lists are owned by the message paginators of stream-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 copies event.channel into channel.data, stream-chat v10 does it (keeping hidden, own_capabilities and member_count).

  • channel.truncated: the SDK no longer clears channel.state.messages, stream-chat v10 truncates the message list.

  • The SDK no longer handles message.updated, message.deleted and reaction.* 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 custom object (it can be empty). Add custom: {} to the attachments you create, for example with AttachmentService.addAttachment:

    attachmentService.addAttachment({
      type: "image",
      image_url: "https://example.com/image.png",
      custom: {},
    });
  • AttachmentUpload.extraData is merged into the attachment, so put custom metadata in extraData.custom.

  • GalleryAttachment (the gallery context of custom attachment templates) has a new, required custom property.

  • DefaultAttachmentData has two new fields: duration and waveform_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 use channel.uploadImage / uploadFile (channel.sendImage / sendFile were removed in v10).

  • AttachmentListComponent compares attachment URLs without their query string. Attachment URLs are signed, and some message updates (for example adding a reaction) renew the signature, so message.attachments can change even if the attachments are the same. AttachmentListComponent ignores these changes, so images and videos aren't reloaded. If you display attachments with your own component instead of stream-attachment-list, you may want to do the same.

Message input and drafts

  • MessageInputComponent.messageDraftChange emits MessageRequest | undefined (it was DraftMessagePayload | undefined, removed in v10). The fields are the same (text, attachments, mentioned_users, poll_id, parent_id, quoted_message_id).
  • MessageInputComponent.loadDraft takes the v10 DraftResponse: draft.message.mentioned_users is now a list of users (it was a list of ids), and dates are TimestampNS. To save a draft with stream-chat v10, call channel.createDraft({ message: draft }).
  • Slash commands come from channel.config.availableCommands (it was channel.getConfig().commands). ComandAutocompleteListItem is now based on Command (it was CommandResponse).

Polls

  • stream-chat v10 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
    PollOption PollOptionResponseData
    PollVote, PollAnswer PollVoteResponseData

    Affected inputs: PollVoteComponent.vote, UpsertAnswerComponent.answer, PollVoteResultsListComponent.option and PollOptionSelectorComponent.option.

  • VotingVisibility is now a string union type ('anonymous' | 'public'), not an enum.

  • Poll, option, vote and answer dates are TimestampNS (unix nanoseconds), convert them with nsToDate.

  • Sort options of poll queries use the array format, for example [{ field: 'created_at', direction: -1 }].

Reactions

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-chat reloads 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-chat v10 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.deleted events with hard_delete remove 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

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 }]