Info:
This is release candidate documentation for Stream Chat React SDK v15. For the latest stable version, see the latest version (v14).

Displaying notifications

Notifications live in client.notifications, in the state layer: who publishes them and what they contain is on Publishing notifications. This page is about showing them. NotificationList components show them on screen, one in each part of the layout, each showing one notification at a time.

Info:

Example

A user pins a message in a thread:

  1. The pin action publishes "Message pinned". Because the action sits inside a Thread, the notification is tagged for the thread panel.
  2. The thread's message list renders a NotificationList for the thread panel. It shows the notification at the bottom of the thread.
  3. The channel's message list renders a NotificationList for the channel panel, and ignores it.
  4. A few seconds after it appears, the notification dismisses itself.

Notification lists and panels

A panel is a part of the screen with its own notification list. The SDK renders one list per panel:

Panel Rendered by
channel MessageList and VirtualizedMessageList inside a Channel
thread MessageList and VirtualizedMessageList inside a Thread
channel-list ChannelNavigation, below its channel lists
thread-list ThreadList
modal each open modal (GlobalModal)

A list shows one notification at a time and queues the rest:

  • A persistent notification goes first. One without a duration, usually one that carries an action such as "Retry", jumps the queue and stays until it's dismissed or a newer persistent one arrives.
  • A repeat replaces its original. When a notification of the same type arrives while one with a duration is showing, the new one takes its place at once.
  • Otherwise the oldest goes next. A notification stays on screen for at least minDisplayMs (1000 ms by default) before a queued one replaces it.
  • The countdown starts when it's seen. A notification's auto-dismiss timer starts when it becomes visible in a list, so one waiting in a queue doesn't expire unseen.

pickNext changes which notification goes next. createDefaultPickNext(pickNewest) keeps the rules above but shows the newest queued notification instead of the oldest.

NotificationList takes these props:

Prop Default What it does
panel the panel the list serves
fallbackPanel 'channel' the panel an untagged notification belongs to
filter (notification) => boolean, run after routing and the display filter
enterFrom 'bottom' the side a notification slides in from: 'bottom', 'top', 'left' or 'right'
verticalAlignment 'bottom' where the notification sits in the list: 'top' or 'bottom'
minDisplayMs 1000 how long a notification stays before a queued one can replace it
pickNext createDefaultPickNext(pickOldest) (notifications, displayed) => Notification | null: what to show next
className a class for the list's element

Replacing the list or a notification

Both are keys in ComponentContext. NotificationList replaces every list, the modal's included, and Notification replaces how each notification renders:

import { forwardRef } from "react";
import type { PropsWithChildren } from "react";
import {
  createDefaultPickNext,
  Notification,
  NotificationList,
  pickNewest,
  WithComponents,
} from "stream-chat-react";
import type {
  NotificationListProps,
  NotificationProps,
} from "stream-chat-react";

const pickNewestFirst = createDefaultPickNext(pickNewest);

// Every list slides in from the top and shows the newest queued notification first.
const TopNotificationList = (props: NotificationListProps) => (
  <NotificationList
    {...props}
    enterFrom="top"
    pickNext={pickNewestFirst}
    verticalAlignment="top"
  />
);

// The list starts a notification's countdown once its element is visible,
// so the component forwards its ref.
const BorderedNotification = forwardRef<HTMLDivElement, NotificationProps>(
  (props, ref) => (
    <Notification {...props} className="bordered-notification" ref={ref} />
  ),
);

const notificationOverrides = {
  Notification: BorderedNotification,
  NotificationList: TopNotificationList,
};

export const WithTopNotifications = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={notificationOverrides}>{children}</WithComponents>
);

Notification takes notification, className, an Icon that receives the notification, showClose, and onDismiss, which defaults to removing the notification. NotificationList passes showClose for persistent notifications, and sets the animation props entryDirection and transitionState itself.

To render notifications your own way, read them with useNotifications({ panel }). It returns system notifications too: drop them with hasSystemNotificationTag. A notification you show this way only expires if you start its countdown, with ensureNotificationTimeout(id) from useNotificationApi().

Styling

Three CSS variables style a notification: --str-chat__notification-background, --str-chat__notification-color and --str-chat__notification-border-radius. Each notification has the class str-chat__notification--<severity>, for example str-chat__notification--error, and each list the class str-chat__notification-list--<panel>:

.str-chat__notification--error {
  --str-chat__notification-background: #b91c1c;
  --str-chat__notification-color: #fff;
}

.str-chat__notification-list--channel-list .str-chat__notification {
  max-width: 250px;
}

Routing notifications

A notification reaches a list through tags of the form target:<panel>:

  • A list shows a notification tagged for its panel. A notification can be tagged for several panels.
  • An untagged notification goes to the channel panel. A list's fallbackPanel prop changes that for the list.
  • A notification from stream-chat carries no tags. When a composer published it, the list goes by the composer: a thread's composer means thread, a channel's means channel. Any other goes to the channel panel.

When a component publishes with useNotificationApi(), addNotification adds the tag for the panel the component sits in: thread inside a Thread, channel inside a Channel, thread-list in the threads view, channel-list inside ChannelLists. targetPanels replaces that guess:

import { useNotificationApi } from "stream-chat-react";

export const CopyInviteLinkButton = ({ link }: { link: string }) => {
  const { addNotification } = useNotificationApi();

  return (
    <button
      onClick={async () => {
        await navigator.clipboard.writeText(link);
        addNotification({
          emitter: "CopyInviteLinkButton",
          message: "Invite link copied",
          severity: "success",
          // Shown in the channel list, wherever the button is.
          targetPanels: ["channel-list"],
        });
      }}
      type="button"
    >
      Copy invite link
    </button>
  );
};

While a modal is open, addNotification also tags the notification for the modal panel, so it appears in the modal instead of behind it.

A list's filter prop narrows what it shows after routing. Declare the filter at module scope: it becomes part of the list's subscription, and a new function on every render resubscribes it.

Display filter

Chat takes a notificationDisplayFilter. Every NotificationList calls it after routing has matched a notification to the list, and before the list's own filter. It receives the notification and the list's panel, and returns whether the list may show it.

The default filter handles modals:

  • While a modal is open, notifications show only in the modal's list. Notifications for other panels are held back until the modal closes.
  • A notification tagged for a modal that has already closed shows in its other panel instead, so a confirmation sent as a dialog closes isn't lost.

A filter you pass replaces the default, modal handling included:

import { Chat } from "stream-chat-react";
import type { NotificationDisplayFilter } from "stream-chat-react";
import type { PropsWithChildren } from "react";

// Keep success confirmations out of the channel list. Every other notification shows.
const notificationDisplayFilter: NotificationDisplayFilter = ({
  notification,
  panel,
}) => !(panel === "channel-list" && notification.severity === "success");

export const App = ({ children }: PropsWithChildren) => (
  <Chat client={client} notificationDisplayFilter={notificationDisplayFilter}>
    {children}
  </Chat>
);

NotificationConfigurationProvider sets a display filter for part of the tree: inside Chat, its displayFilter replaces the one from above for every list below it. useNotifications applies the display filter only when you pass applyDisplayFilter: true.

Translating notifications

The Notification component translates each notification as it renders it, through the notification translation topic:

  1. A translator registered for the notification's type decides the text.
  2. Otherwise the SDK's own translators do. There is one for every type stream-chat publishes, and a few for types the React components publish. They render keys in the notification.* namespace.
  3. Otherwise the list renders notification.message as it is.

The React components mostly translate their notifications before publishing them: "Message pinned" is t('common.messagePinned.label'). So there are three ways to change the text:

  • A notification from a React component: override the translation key it was published with. When a message action fails with an error message of its own, the notification shows that message. An audio playback error always shows notification.audioPlaybackError.
  • A notification from stream-chat: override its notification.* key.
  • Text that depends on the notification: register a translator for its type.
import { asDynamicKey, Streami18n } from "stream-chat-react";
import type {
  NotificationTranslatorOptions,
  Translator,
} from "stream-chat-react";

const i18nInstance = new Streami18n({
  translationsForLanguage: {
    // Published by stream-chat: an upload that failed.
    "notification.attachmentUploadFailed": "That file could not be uploaded",
    // Published by the pin action, translated at the call site.
    "common.messagePinned.label": "Pinned",
  },
});

// Names the author of the pinned message.
const pinTranslators: Record<
  string,
  Translator<NotificationTranslatorOptions>
> = {
  "api:message:pin:success": ({ options: { notification }, t }) => {
    const message = notification?.origin.context?.message as
      { user?: { name?: string } } | undefined;
    return t(asDynamicKey("myApp.notification.messagePinned"), {
      author: message?.user?.name ?? "someone",
      defaultValue: "Pinned a message from {{ author }}",
    });
  },
};

i18nInstance.translationBuilder.registerTranslators(
  "notification",
  pinTranslators,
);

Pass i18nInstance to Chat. A translator receives the notification as options.notification, and returns a string, or null to show notification.message as it is. Keys of your own go through asDynamicKey: see Localization for the translation builder and dictionaries.

System notifications

A system notification is about the whole app, not a panel: "Waiting for network…" or "Reconnecting…". It carries the system tag and no target: tags, and no NotificationList shows it.

Chat publishes one while the client can't reach Stream:

  • "Waiting for network…" as soon as the device reports it has no network;
  • "Reconnecting…" when the socket has been down for client.wsConnection.config.offlineNotificationDisplayDelayMs. A drop shorter than that shows nothing.

Both have the type system:network:connection:lost and the severity loading, stay until the connection is back, and then disappear. Chat renders no banner for them. Render your own with useSystemNotifications, anywhere inside Chat:

import type { Notification } from "stream-chat";
import { useSystemNotifications } from "stream-chat-react";

const isConnectionLost = (notification: Notification) =>
  notification.type === "system:network:connection:lost";

export const ConnectionBanner = () => {
  const [lostConnection] = useSystemNotifications({ filter: isConnectionLost });
  if (!lostConnection) return null;

  return (
    <div className="connection-banner" role="status">
      {lostConnection.message}
    </div>
  );
};

The message is already translated: Chat publishes it with t(). To publish a system notification of your own, call addSystemNotification from useNotificationApi(): it returns the id to remove it with. The System notification banner recipe builds a complete banner, and Connection status explains the connection states behind it.

What the React components publish

These components publish notifications. Each one sets emitter to the name in brackets:

Component Types
Message actions (MessageActions) api:message:pin:*, api:message:unpin:*, api:message:markUnread:*, api:message:flag:*, api:message:delete:*, api:user:mute:*, api:user:unmute:*
Reminders and save for later (MessageActions) api:message:reminder:set:*, api:message:reminder:delete:*, api:message:saveForLater:create:*, api:message:saveForLater:delete:*
Channel-list row actions (ChannelListItemActionButtons) api:channel:archive:*, api:channel:unarchive:success, api:channel:pin:*, api:channel:unpin:success, api:channel:mute:*, api:channel:unmute:success, api:channel:leave:*, api:user:ban:*, api:user:unban:success
Channel details actions (ChannelManagementView, ChannelMemberDetail, ChannelMembersView) api:channel:mute:*, api:channel:unmute:*, api:channel:leave:*, api:channel:delete:*, api:channel:addMembers:*, api:channel:remove-members:*, api:channel:watch:failed, api:user:mute:*, api:user:unmute:*, api:user:block:*, api:user:unblock:*
Editing a channel in its details (ChannelManagementEditBody) channel:channel:update:success, api:channel:update:failed
Polls (PollCreationDialog, EndPollAlert) api:poll:create:success, api:poll:end:*
Location (ShareLocationDialog) api:location:share:failed, browser:location:get:failed
Audio recording (AudioRecorder) audioRecording:cancel:success
Audio playback (AudioPlayer) browser:audio:playback:error
Reactions (Reactions) api:message:reactions:fetch:failed
Search results (ChannelSearchResultItem, MessageSearchResultItem, UserSearchResultItem) api:channel:watch:failed, when the opened channel fails to load
Viewing a reply in its channel (useMessageAlsoSentInChannelNavigation) api:message:search:not-found
Channel list (ChannelList) no type: the failed query's error message, untranslated
Lost connection (Chat) system:network:connection:lost, a system notification

* stands for success and failed. The pin, unpin, delete, mute and unmute message actions are the exception: their handlers catch the request error, so only the success type is published, and a failed request still shows the success notification.

Most messages are translated with t() before they're published. Three kinds aren't:

  • A failed message action whose error carries a message shows that message.
  • A failed reminder or save for later, without such a message, shows English text.
  • The channel list shows the failed query's error message.

The search results, the reply view and the channel list publish without a panel tag, so their notifications show in the channel panel.

Uploads, sending, message-list jumps and poll votes are published by stream-chat: see Publishing notifications.

Used in: Notifications and Connection status.