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 as a snackbar, one in each part of the app, each showing one notification at a time.
Example
A user pins a message in a channel:
- The pin action publishes "Message pinned". Because the action sits inside a
Channel, the notification is tagged for that channel's notification host. - The channel's
MessageListrenders aNotificationListfor the same host. It shows the notification at the bottom of the message list. - The
ChannelListon the screen below renders aNotificationListfor a host of its own, and ignores it. - Three seconds after it appears, the notification dismisses itself.
Notification lists and targets
A notification target is a part of the app with its own notification list. It has a panel, the kind of surface, and a hostId, which tells apart two surfaces of the same kind, such as two channels open on the stack. The SDK sets a target around each of its surfaces and renders a list inside it:
| Surface | panel |
hostId by default |
List rendered by |
|---|---|---|---|
Channel |
channel |
channel:<cid>, from getChannelNotificationHostId(cid) |
its MessageList |
Thread |
thread |
thread:<id>, from getThreadNotificationHostId(id) |
its MessageList |
ChannelList |
channel-list |
a random id for each mounted list | ChannelList |
ThreadList |
thread-list |
a random id for each mounted list | ThreadList |
ChannelDetails, and its own screens |
channel-details |
one for the screen and one for each screen it opens | each of those screens |
Channel, Thread, ChannelList and ThreadList take a notificationHostId prop that replaces the default. Set it when two of them should share a list, or when your code needs to know the id to publish to it.
The screens channel details opens, such as the member list and the media list, are native modals that cover the screen, so each one has its own target and list.
A list shows one notification at a time:
- A persistent notification goes first. One without a duration, usually one with severity
loading, shows until it's dismissed, ahead of any newer one. - Otherwise the newest one shows. In both cases the list removes every other notification for its target from the store. There is no queue.
- The countdown starts when it shows. A notification's timer starts when a list shows it, so one waiting for its list doesn't expire unseen. A notification with actions stays at least 5 seconds.
- Leaving clears the list. When a list unmounts, for example when the user navigates back from a channel, it removes every notification for its target.
NotificationList takes these props:
| Prop | Default | What it does |
|---|---|---|
panel, hostId |
the target the list serves. Without them, the nearest NotificationTargetProvider's target |
|
filter |
(notification) => boolean, run after routing |
|
enterFrom |
'bottom' |
the side a notification animates in from: 'bottom', 'top', 'left' or 'right' |
verticalAlignment |
'bottom' |
the edge the list is anchored to: 'top' or 'bottom' |
bottomOffset |
extra distance from the bottom edge | |
topOffset |
extra distance from the top edge, with verticalAlignment="top" |
A notification's metadata.entryDirection or origin.context.entryDirection overrides enterFrom for that notification.
MessageList passes two props itself. With a floating composer, bottomOffset keeps the snackbar above it. With pending uploads on, filter is excludeCanceledUploadNotifications, which hides the upload failure a user causes by removing an attachment while it uploads.
Replacing the list or a notification
Three keys of ComponentsContext render notifications. NotificationList replaces the lists of message lists, ChannelList and ThreadList; the channel details screens render the default list directly. Notification replaces how each notification renders, and NotificationIcon replaces only its icon:
import { StyleSheet, View } from "react-native";
import { Notification, NotificationList } from "stream-chat-react-native";
import type {
ComponentOverrides,
NotificationListProps,
NotificationProps,
} from "stream-chat-react-native";
// Every list animates in from the top, under the screen's header.
const TopNotificationList = (props: NotificationListProps) => (
<NotificationList
{...props}
enterFrom="top"
topOffset={8}
verticalAlignment="top"
/>
);
const BorderedNotification = (props: NotificationProps) => (
<View style={styles.border}>
<Notification {...props} />
</View>
);
export const notificationOverrides: ComponentOverrides = {
Notification: BorderedNotification,
NotificationList: TopNotificationList,
};
const styles = StyleSheet.create({
border: { borderColor: "#0d47a1", borderRadius: 16, borderWidth: 2 },
});Spread the props you receive: MessageList passes its bottomOffset and filter through them.
Notification takes:
| Prop | What it is |
|---|---|
notification |
the notification, of type Notification from stream-chat |
onDismiss |
called by the close button. Without it, the notification is removed from the store |
showClose |
shows the close button. A persistent notification always has one |
Icon |
an icon component that receives { notification }. Without it, the NotificationIcon key's |
entryDirection |
the side the list animated the notification in from |
The default renders the icon for the severity, the translated message, a button for each of notification.actions, and the close button. On Android, TalkBack announces it through a live region, assertively for errors and politely otherwise. accessibilityLiveRegion is Android only, so VoiceOver on iOS doesn't announce it.
To render notifications your own way, drive a list with useNotificationListController({ filter, hostId, panel }). It does what NotificationList does, picking the notification, starting its timer and clearing on unmount, and returns { notification, dismissNotification } for you to render. Render it inside a target: inside one of the SDK's surfaces, or inside a NotificationTargetProvider of your own.
Styling
The theme styles the default components: notification for each notification (container, contentContainer, iconContainer, message, actionsContainer, actionButton, closeButton), and notificationList.container for the list. The theme also has notification.actionButtonText, which the default components don't apply. See Theming.
Routing notifications
A notification reaches a list through tags of the form target:<panel>:<hostId>, which name one host, or target:<panel>, which name every host of a panel.
When a component publishes with useNotificationApi(), addNotification tags the notification for the target the component sits in: inside a Channel, that channel's host. Two options replace that:
target: { panel, hostId }sends it to one host.targetPanels: [...]sends it to every host of those panels.
import { Pressable, Text } from "react-native";
import { useNotificationApi } from "stream-chat-react-native";
export const SaveDraftButton = ({
onSave,
}: {
onSave: () => Promise<void>;
}) => {
const { addNotification } = useNotificationApi();
return (
<Pressable
accessibilityRole="button"
onPress={async () => {
await onSave();
addNotification(
{
message: "Draft saved",
options: { severity: "success", type: "myApp:draft:save:success" },
origin: { emitter: "SaveDraftButton" },
},
// Shown in every channel list, wherever the button is.
{ targetPanels: ["channel-list"] },
);
}}
>
<Text>Save draft</Text>
</Pressable>
);
};A notification that arrives with no target tag, from stream-chat or from your own call to client.notifications.add(), is tagged when it's added:
- From a composer: the composer's thread or channel. An upload that failed in a thread's composer shows in that thread.
- During
runWithNotificationTarget(callback, target): the target the callback runs in. - Otherwise: the notification list that mounted most recently, which is usually the one on the screen the user is looking at.
useNotificationApi() returns addNotification, addSystemNotification, removeNotification(id), removeNotificationsForCurrentPanel(), runWithNotificationTarget and startNotificationTimeout(id, durationOverride?). A list's filter prop narrows what it shows after routing.
Translating notifications
The Notification component chooses its text by the notification's type:
- A type
stream-chatpublishes, such asapi:attachment:upload:failed, renders a translation key, most of them in thenotifications.*namespace, throught(). The SDK maps every typestream-chatdefines. - A few types the SDK publishes,
api:poll:end:success,api:poll:end:failed,browser:audio:playback:errorandchannel:jumpToFirstUnread:failed, are mapped the same way. - Any other type renders
notification.messageas it is.
The SDK's components translate most of their notifications before publishing them: "Message pinned" is t("message.pinned.text", "Message pinned"). So there are two ways to change the text:
- A notification from an SDK component: override the key it was published with. When a message action fails with an error that has a message, the notification shows that message instead.
- A notification from
stream-chat: override itsnotifications.*key. Some types have two, one used when the notification carries a reason and one without: for an upload,notifications.attachmentUploadFailed.errorandnotifications.attachmentUploadFailed.withReason.error.
import { Streami18n } from "stream-chat-react-native";
export const streami18n = new Streami18n({
translationsForLanguage: {
"message.pinned.text": "Pinned",
"notifications.attachmentUploadFailed.error":
"That file could not be uploaded",
"notifications.attachmentUploadFailed.withReason.error":
"That file could not be uploaded: {{reason}}",
},
});Pass the instance to Chat and OverlayProvider as i18nInstance. Text that depends on the notification beyond these keys needs your own Notification component, or your own publishing. Dictionaries are on Localization.
System notifications
A system notification is about the whole app, not a surface: "Syncing your data", or "Your session expires soon". It carries the system tag and no target tags, and no NotificationList shows it. Publish one with addSystemNotification from useNotificationApi(), which returns its id, and read them with useSystemNotifications, anywhere inside Chat:
import { Text, View } from "react-native";
import type { Notification } from "stream-chat";
import { useSystemNotifications } from "stream-chat-react-native";
const isSync = (notification: Notification) =>
notification.type === "myApp:sync:running";
export const SyncBanner = () => {
const [sync] = useSystemNotifications({ filter: isSync });
if (!sync) return null;
return (
<View accessibilityLiveRegion="polite">
<Text>{sync.message}</Text>
</View>
);
};Publish it with addSystemNotification({ message: "Syncing...", options: { severity: "loading", type: "myApp:sync:running" }, origin: { emitter: "Sync" } }), and remove it with removeNotification(id) when the work is done. A loading notification has no duration, so it stays until you remove it.
useNotifications({ target, filter }) reads the notifications for lists: pass a target to read one host's, which leaves out system notifications. Without a target it returns every notification in the store, system ones included, so drop those in your filter if you don't want them. Both hooks build their selector from filter, so declare it at module scope, as isSync is.
Connection status
The SDK publishes no notification when the connection drops. It shows the connection's state in its own components instead:
NetworkDownIndicator, at the top of each message list: "Waiting for network..." while the device has no network, and "Reconnecting..." while the WebSocket is down. A WebSocket drop shorter thanclient.wsConnection.config.offlineNotificationDisplayDelayMsshows nothing.ChannelListHeaderNetworkDownIndicator, at the top of each channel list, with "Reconnecting..." in both cases.- Screen-reader announcements of "Offline", "Reconnecting" and "Connected", while a
ChannelorThreadis mounted, whenOverlayProviderhasaccessibility={{ enabled: true }}andannounceConnectionStateisn'tfalse. See Accessibility.
Replace the two indicators through WithComponents, or read the state yourself with useNetworkConnectionState() and useSettledWSConnectionHealth(). Connection status explains the states behind them.
What the SDK publishes
These components publish notifications. Each one sets origin.emitter to the name in brackets:
| Component | Types |
|---|---|
Message actions (MessageActions) |
clipboard:message:copy:*, api:message:delete:*, api:message:pin:*, api:message:unpin:*, api:message:flag:*, api:message:markUnread:*, api:user:mute:*, api:user:unmute:* |
Channel actions (ChannelActions), in the channel list and channel details |
api:channel:pin:*, api:channel:unpin:success, api:channel:archive:*, api:channel:unarchive:success, api:channel:mute:*, api:channel:unmute:success, api:channel:leave:*, api:channel:delete:*, api:channel:add-members:*, api:channel:remove-members:*, api:channel:update-name:*, api:channel:update-image:*, api:user:mute:*, api:user:unmute:*, api:user:block:*, api:user:unblock:success |
Member actions (UserActions) |
api:user:mute:*, api:user:unmute:*, api:user:block:*, api:user:unblock:success |
Channel details lists (ChannelMemberList, AddChannelMembers, ChannelMediaList, ChannelFileAttachmentList, ChannelPinnedMessageList) |
api:channel:query-members:failed, api:channel:query-users:failed, api:channel:query-media:failed, api:channel:query-file-attachments:failed, api:channel:query-pinned-messages:failed |
Polls (PollActions) |
api:poll:end:* |
Voice recording (AudioRecorder, AudioRecordingButton) |
audioRecording:cancel:success, validation:audio:recording:hold-required, permission:audio:recording:blocked |
Audio playback (AudioPlayer) |
browser:audio:playback:error |
Reactions (Reactions) |
api:message:reactions:fetch:failed |
Jumping to the first unread message (Channel, UnreadMessagesNotification) |
channel:jumpToFirstUnread:failed |
* stands for success and failed. Unpinning, unarchiving, unmuting a channel and unblocking a user publish success under their own type, and a failure under the type of the action they undo, such as api:channel:pin:failed for a failed unpin.
Most messages are translated with t() before they're published. A failed message action whose error carries a message shows that message instead.
Uploads, sending, message-list jumps, poll votes and commands are published by stream-chat: see Publishing notifications.
Pitfalls
- A notification published outside every target. With no target, it goes to the list that mounted last, which may be on another screen than the component that published it. Publish from inside the surface it belongs to, or pass
targetortargetPanels. - Two surfaces with the same host. Two
Channels for the same channel, mounted at once, sharechannel:<cid>, and both lists show its notifications. Give one a differentnotificationHostId. - A replaced
NotificationListthat drops its props.MessageListpassesbottomOffsetandfilterthrough them. Spread them. - Matching on
notification.message. It's display text, and forstream-chat's notifications untranslated English. Match ontype. - An inline
filterforuseNotificationsoruseSystemNotifications. It's part of the hook's subscription. Declare it at module scope.
Used in: Notifications and Connection status.