Publishing notifications

client.notifications is the client's notification manager, the service through which notifications are published and managed. A notification is an object that describes what just happened, such as a failed upload. It carries the text to show, such as "Error uploading attachment", its severity, who published it, and a type that identifies what it's about. A UI uses the type to show a translated text instead. The SDK publishes one when an upload, a send or another operation goes wrong, and your code can publish its own. The manager keeps the current notifications in its store, and a UI reads them from there and shows each one for a while, or until the user closes it. Use this page to publish notifications of your own, to translate or filter the SDK's by type, and to control how long each one stays.

Info:

Example

The user adds a 300 MB video to a message, but the Stream app accepts files of up to 100 MB:

  1. The composer's upload middleware refuses the file and calls client.notifications.addError().
  2. The notification goes into client.notifications.store, with the severity error, the type validation:attachment:upload:blocked and the reason size_limit in its metadata.
  3. The SDK's NotificationList shows it, and starts its countdown with client.notifications.startTimeout(id). After three seconds it's removed.

How notifications are shown, where each one appears, and the ones the SDK's components publish, are on Notifications UI.

Publishing a notification

client.notifications.add() publishes a notification and returns its id. It takes the message to show, an origin that says who published it, and options:

const id = client.notifications.add({
  message: "Settings saved",
  origin: { emitter: "SettingsScreen" },
  options: { severity: "success", type: "app:settings:save:success" },
});

client.notifications.addError(), client.notifications.addWarning(), client.notifications.addInfo() and client.notifications.addSuccess() do the same with the severity set:

const saveSettings = async (save: () => Promise<void>) => {
  try {
    await save();
    client.notifications.addSuccess({
      message: "Settings saved",
      origin: { emitter: "SettingsScreen" },
    });
  } catch (error) {
    client.notifications.addError({
      message: "Could not save the settings",
      origin: { context: { section: "profile" }, emitter: "SettingsScreen" },
      options: {
        originalError: error instanceof Error ? error : undefined,
        type: "app:settings:save:failed",
      },
    });
  }
};

What a notification carries:

Field What it holds
message The text. The SDK's own messages are plain English for developers; a UI should translate by type.
origin.emitter The publisher, such as "AttachmentManager", so a UI can pick out some publishers.
origin.context What a UI needs to know, such as the attachment. Optional.
options.severity 'error', 'warning', 'info', 'success', or a string of your own.
options.type What the notification is about. See Notification types.
options.actions The data for the action buttons a UI shows with the notification, each { label, handler }.
options.duration How long to show it, in milliseconds.
options.metadata Extra data, such as the reason for a failure.
options.tags Labels a UI can use to decide where to show it.
options.originalError The error that caused it.

The SDK shows notifications per screen area, such as the channel list, a channel or a thread, and reads where each one belongs from its tags. In a component, useNotificationApi().addNotification() publishes to the same list and tags the notification for the area the component sits in. Notifications UI explains the targets.

Notification types

A notification's type says what it's about, so a UI can translate it or group it. The types stream-chat publishes follow the pattern domain:entity:operation:result:

  • domain: where it happened, such as api for a request or validation for a check before one.
  • entity: what it's about, such as attachment, poll or message.
  • operation: what was attempted, such as upload, create or castVote.
  • result: how it went, such as failed, blocked, limit or success.

So api:attachment:upload:failed is an upload request that failed. Every type stream-chat publishes is in CORE_NOTIFICATION_TYPE, and the values don't change between minor releases. For your own notifications, any string works, but the same pattern keeps them easy to tell apart. Match on type, never on message: the message is a fallback whose wording can change. The SDK translates the stream-chat notifications it shows by their type, as Localization explains.

Severity, actions and durations

The severity decides how long a notification stays: three seconds for each of error, warning, info and success by default. Change the durations in the client's configuration. Naming one severity keeps the others:

client.config.set({
  client: { notifications: { durations: { error: 8000 } } },
});

A notification disappears on its own after its duration, which is options.duration if it has one, or else the configured duration for its severity. It stays until it's removed when:

  • its duration is 0;
  • it has no duration and no severity;
  • its severity is one of your own, such as 'critical', with no duration configured for it.
import { Linking } from "react-native";
import { StreamChat } from "stream-chat";

const client = StreamChat.getInstance("YOUR_API_KEY");

client.notifications.addWarning({
  message: "You're offline. Messages will be sent when you reconnect.",
  origin: { emitter: "ConnectionBanner" },
  options: {
    actions: [
      { handler: () => void Linking.openSettings(), label: "Open settings" },
    ],
    duration: 0,
  },
});

A notification can also offer actions, which are things the user can do about it, such as Open settings in the example above. Each action in options.actions is { label, handler }. The manager only stores them with the notification. Your UI shows each one as a button with its label, and calls its handler when the user presses it:

import { Pressable, Text, View } from "react-native";
import type { Notification } from "stream-chat";
import { useChatContext } from "stream-chat-react-native";

export const NotificationActions = ({
  notification,
}: {
  notification: Notification;
}) => {
  const { client } = useChatContext();

  return (
    <View style={{ flexDirection: "row" }}>
      {(notification.actions ?? []).map(({ handler, label }) => (
        <Pressable
          accessibilityRole="button"
          key={label}
          onPress={() => {
            handler();
            // The user acted on the notification, so it can go.
            client.notifications.remove(notification.id);
          }}
        >
          <Text>{label}</Text>
        </Pressable>
      ))}
    </View>
  );
};

Following and managing notifications

The countdown doesn't start on its own. The UI starts it when it shows the notification, so a notification nobody has seen yet isn't removed.

To Call What happens
follow the list subscribe to client.notifications.store It holds the list as { notifications }, like any store. useNotifications() returns it in a component.
start the countdown when the notification is shown client.notifications.startTimeout(id) The notification is removed once its duration has passed.
start the countdown only if it isn't running yet client.notifications.ensureTimeout(id) Same as startTimeout(), but a running countdown isn't restarted.
pause the countdown, for example while the user interacts with the notification client.notifications.clearTimeout(id) The countdown stops. Start it again to resume.
remove one notification client.notifications.remove(id) It leaves the list.
remove them all client.notifications.clear() The list is empty.

Who publishes notifications

stream-chat and the SDK's components publish notifications of their own, and your code publishes with the same calls. Notifications lists every one they publish, with its type, severity, origin.emitter and when it's published. The message list's emitters carry the method name, such as MessagePaginator.jumpToMessage.

With offline support, a send or an edit that fails because the device is offline publishes api:message:send:failed or api:message:update:failed too, although it waits as a pending task and is sent after the next reconnect.

Pitfalls

  • Matching on the message. The SDK's messages are English fallbacks and can be reworded. Use type.
  • A notification that never goes away. The manager removes a notification only when its countdown ends, and the countdown starts only when client.notifications.startTimeout(id) is called. The SDK's notification components call it when they show a notification. A UI of your own has to call it too, or the notification stays in the list.
  • Durations set in the wrong place. In client.config.set(), the durations go under client.notifications.durations, as in { client: { notifications: { durations: { error: 8000 } } } }. A top-level notifications key isn't a setting, so the durations stay at their defaults.

Used in