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.
Example
The user adds a 300 MB video to a message, but the Stream app accepts files of up to 100 MB:
- The composer's upload middleware refuses the file and calls
client.notifications.addError(). - The notification goes into
client.notifications.store, with the severityerror, the typevalidation:attachment:upload:blockedand the reasonsize_limitin its metadata. - The SDK's
NotificationListshows it, and starts its countdown withclient.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 asapifor a request orvalidationfor a check before one.entity: what it's about, such asattachment,pollormessage.operation: what was attempted, such asupload,createorcastVote.result: how it went, such asfailed,blocked,limitorsuccess.
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 underclient.notifications.durations, as in{ client: { notifications: { durations: { error: 8000 } } } }. A top-levelnotificationskey isn't a setting, so the durations stay at their defaults.