# Displaying notifications

> **Info:** This is release candidate documentation for **Stream Chat React Native SDK v10**. For the latest stable version, see the [latest version (v9)](https://getstream.io/chat/docs/sdk/react-native/).

Notifications live in `client.notifications`, in the state layer: who publishes them and what they contain is on [Publishing notifications](https://getstream.io/chat/docs/sdk/react-native/v10/state/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](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-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:

```tsx
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](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/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.

```tsx
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`.

```ts
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](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/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`:

```tsx
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](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/accessibility/).

Replace the two indicators through `WithComponents`, or read the state yourself with `useNetworkConnectionState()` and `useSettledWSConnectionHealth()`. [Connection status](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/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](https://getstream.io/chat/docs/sdk/react-native/v10/state/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 `Channel`s 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](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/notifications/) and [Connection status](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/connection-status/).

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/displaying-notifications/](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/displaying-notifications/).