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.
Example
A user pins a message in a thread:
- The pin action publishes "Message pinned". Because the action sits inside a
Thread, the notification is tagged for thethreadpanel. - The thread's message list renders a
NotificationListfor thethreadpanel. It shows the notification at the bottom of the thread. - The channel's message list renders a
NotificationListfor thechannelpanel, and ignores it. - 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
typearrives 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
channelpanel. A list'sfallbackPanelprop changes that for the list. - A notification from
stream-chatcarries no tags. When a composer published it, the list goes by the composer: a thread's composer meansthread, a channel's meanschannel. Any other goes to thechannelpanel.
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:
- A translator registered for the notification's
typedecides the text. - Otherwise the SDK's own translators do. There is one for every type
stream-chatpublishes, and a few for types the React components publish. They render keys in thenotification.*namespace. - Otherwise the list renders
notification.messageas 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 itsnotification.*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.