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

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.

Info:

Example

A user pins a message in a channel:

  1. The pin action publishes "Message pinned". Because the action sits inside a Channel, the notification is tagged for that channel's notification host.
  2. The channel's MessageList renders a NotificationList for the same host. It shows the notification at the bottom of the message list.
  3. The ChannelList on the screen below renders a NotificationList for a host of its own, and ignores it.
  4. 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:

  1. From a composer: the composer's thread or channel. An upload that failed in a thread's composer shows in that thread.
  2. During runWithNotificationTarget(callback, target): the target the callback runs in.
  3. 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:

  1. A type stream-chat publishes, such as api:attachment:upload:failed, renders a translation key, most of them in the notifications.* namespace, through t(). The SDK maps every type stream-chat defines.
  2. A few types the SDK publishes, api:poll:end:success, api:poll:end:failed, browser:audio:playback:error and channel:jumpToFirstUnread:failed, are mapped the same way.
  3. Any other type renders notification.message as 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 its notifications.* key. Some types have two, one used when the notification carries a reason and one without: for an upload, notifications.attachmentUploadFailed.error and notifications.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 than client.wsConnection.config.offlineNotificationDisplayDelayMs shows nothing.
  • ChannelListHeaderNetworkDownIndicator, at the top of each channel list, with "Reconnecting..." in both cases.
  • Screen-reader announcements of "Offline", "Reconnecting" and "Connected", while a Channel or Thread is mounted, when OverlayProvider has accessibility={{ enabled: true }} and announceConnectionState isn't false. 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 target or targetPanels.
  • Two surfaces with the same host. Two Channels for the same channel, mounted at once, share channel:<cid>, and both lists show its notifications. Give one a different notificationHostId.
  • A replaced NotificationList that drops its props. MessageList passes bottomOffset and filter through them. Spread them.
  • Matching on notification.message. It's display text, and for stream-chat's notifications untranslated English. Match on type.
  • An inline filter for useNotifications or useSystemNotifications. It's part of the hook's subscription. Declare it at module scope.

Used in: Notifications and Connection status.