In-App Notifications

The Stream Chat SDK for React Native provides a built-in way to display in-app notifications to users using a Toaster or their own custom implementation. This feature enhances user experience by allowing users to get feedback for certain actions without leaving the app.

In app notifications

In this cookbook, we will show you how to use the built-in toaster to display in-app notifications.

Store

The Stream Chat SDK for React Native provides a built-in store to display in-app notifications that manages the state of the notifications. The store is used in the useInAppNotificationsState hook and is exposed from the SDK.

const { openInAppNotification, closeInAppNotification, notifications } =
  useInAppNotificationsState();

The hook returns the following methods:

  • openInAppNotification: Opens a new notification. This can be used to display a notification for a specific action.
  • closeInAppNotification: Closes a notification. Calling this method will remove the notification from the store.
  • notifications: A list of all the notifications. This is a read-only list of the notifications that are currently displayed.

This hook can be easily used to display a notification for a specific action in the form of a Toaster.

Creating a Custom Toaster

To create a custom toaster, you can use the useInAppNotificationsState hook to display a notification for a specific action.

An example of a custom toaster is the following:

import {
  Dimensions,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
} from "react-native";
import Animated, {
  Easing,
  SlideInDown,
  SlideOutDown,
} from "react-native-reanimated";
import {
  SafeAreaView,
  useSafeAreaInsets,
} from "react-native-safe-area-context";
import { useInAppNotificationsState, useTheme } from "stream-chat-react-native";
import type { Notification } from "stream-chat";

const { width } = Dimensions.get("window");

const severityIconMap: Record<Notification["severity"], string> = {
  error: "❌",
  success: "✅",
  warning: "⚠️",
  info: "ℹ️",
};

export const Toast = () => {
  const { closeInAppNotification, notifications } =
    useInAppNotificationsState();
  const { top } = useSafeAreaInsets();
  const {
    theme: {
      colors: { overlay, white_smoke },
    },
  } = useTheme();

  return (
    <SafeAreaView style={[styles.container, { top }]} pointerEvents="box-none">
      {notifications.map((notification) => (
        <Animated.View
          key={notification.id}
          entering={SlideInDown.easing(Easing.bezierFn(0.25, 0.1, 0.25, 1.0))}
          exiting={SlideOutDown}
          style={[styles.toast, { backgroundColor: overlay }]}
        >
          <View style={[styles.icon, { backgroundColor: overlay }]}>
            <Text style={[styles.iconText, { color: white_smoke }]}>
              {severityIconMap[notification.severity]}
            </Text>
          </View>
          <View style={styles.content}>
            <Text style={[styles.message, { color: white_smoke }]}>
              {notification.message}
            </Text>
          </View>
          <TouchableOpacity
            onPress={() => closeInAppNotification(notification.id)}
          >
            <Text style={[styles.close, { color: white_smoke }]}>✕</Text>
          </TouchableOpacity>
        </Animated.View>
      ))}
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    position: "absolute",
    right: 16,
    left: 16,
    alignItems: "flex-end",
  },
  toast: {
    width: width * 0.9,
    borderRadius: 12,
    padding: 12,
    marginBottom: 8,
    flexDirection: "row",
    justifyContent: "space-between",
    alignItems: "center",
    shadowColor: "#000",
    shadowOpacity: 0.2,
    shadowRadius: 4,
    elevation: 5,
  },
  content: {
    flex: 1,
    marginHorizontal: 8,
  },
  message: {
    fontSize: 14,
    fontWeight: "600",
  },
  close: {
    fontSize: 16,
  },
  icon: {
    width: 20,
    height: 20,
    borderRadius: 12,
    justifyContent: "center",
    alignItems: "center",
  },
  iconText: {
    fontWeight: "bold",
    includeFontPadding: false,
  },
  warning: {
    backgroundColor: "yellow",
  },
});

This example uses the Notification type from the stream-chat package to display the notification.

Triggering a notification

You can trigger the notification by calling the openInAppNotification method with the notification object.

openInAppNotification({
  id: "1",
  message: "This is a test notification",
  severity: "info",
  created_at: Date.now(),
  origin: {
    emitter: "test_emitter",
    id: "test_id",
  },
});

Configuring the Toaster

The Toast component should be added in the highest hierarchy of the app where you want to display the notifications.

<Toast />

Closing a Toaster

The toaster will automatically close after a certain amount of time. You can also close the toaster manually by calling the closeInAppNotification method.

Client side notifications

To listen to client side notifications, you can use the useClientNotifications hook exposed from the SDK.

const { notifications } = useClientNotifications();

This hook returns the following methods:

  • notifications: A list of all the notifications. This is a read-only list of the notifications that are currently displayed.

Depending on the difference in the notifications list you can trigger the toaster to display a notification.

In our Sample app, we created a useClientNotificationsHandler hook that listens to the client side notifications and triggers the toaster to display a notification.

This is how it looks like:

import type { Notification } from "stream-chat";
import {
  useClientNotifications,
  useInAppNotificationsState,
} from "stream-chat-react-native";

import { useEffect, useMemo, useRef } from "react";

export const usePreviousNotifications = (notifications: Notification[]) => {
  const prevNotifications = useRef<Notification[]>(notifications);

  const difference = useMemo(() => {
    const prevIds = new Set(
      prevNotifications.current.map((notification) => notification.id),
    );
    const newIds = new Set(
      notifications.map((notification) => notification.id),
    );
    return {
      added: notifications.filter(
        (notification) => !prevIds.has(notification.id),
      ),
      removed: prevNotifications.current.filter(
        (notification) => !newIds.has(notification.id),
      ),
    };
  }, [notifications]);

  prevNotifications.current = notifications;

  return difference;
};

/**
 * This hook is used to open and close the notifications when the notifications are added or removed.
 * @returns {void}
 */
export const useClientNotificationsHandler = () => {
  const { notifications } = useClientNotifications();
  const { openInAppNotification, closeInAppNotification } =
    useInAppNotificationsState();
  const { added, removed } = usePreviousNotifications(notifications);

  useEffect(() => {
    added.forEach(openInAppNotification);
    removed.forEach((notification) => closeInAppNotification(notification.id));
  }, [added, closeInAppNotification, openInAppNotification, removed]);
};
© Getstream.io, Inc. All Rights Reserved.