Message Reminders

The Stream Chat SDK for React Native supports message reminders, allowing users to set reminders for messages they want to revisit later. This feature is particularly useful for keeping track of important messages or tasks.

Message reminders allow users to set reminders for messages. This feature is useful when users want to be notified about messages that they need to follow up on. The article explains how message reminders are managed by the React SDK.

When a reminder includes a timestamp, it’s like saying “remind me later about this message,” and the user who set it will receive a notification at the designated time. If no timestamp is provided, the reminder functions more like a bookmark, allowing the user to save the message for later reference.

Push Notifications Setup

Reminders require Push V3 to be enabled in your Stream Chat app. To do so, you can go to Push Notifications section in the Stream Dashboard and enable Push V3 by clicking on Upgrade to V3.

Push V3

After enabling, you need to go to individual configurations, scroll down to Configure Push Notification Templates and enable event specific notifications for notification.reminder_due event.

Message Reminder UI Interaction

Users can create, update or delete message reminders by accessing the relevant message action from the message actions menu:

Then the message reminder notification is displayed as a part of the message UI (or removed from the UI if the reminder has been deleted):

Get Message Reminder Data

The React Native SDK components use the client.reminders controller to perform the CRUD operations. The controller keeps reactive state - a mapping of message id to Reminder instances. Reminder instances have in turn their own reactive state that extends the ReminderResponse from the server with timeLeftMs.

export type ReminderState = {
  channel_cid: string;
  created_at: Date;
  message: MessageResponse | null;
  message_id: string;
  remind_at: Date | null;
  timeLeftMs: number | null;
  updated_at: Date;
  user: UserResponse | null;
  user_id: string;
};
  1. Getting the reminder for a specific message

For this, you can use the useMessageReminder hook from the SDK, which returns a Reminder instance for the specified message ID.

We can subscribe to the reminder state changes on two levels:

  1. Subscribe to a specific reminder state changes
import { useMessageReminder, useStateStore } from "stream-chat-react";
import type { LocalMessage, ReminderState } from "stream-chat";

const reminderStateSelector = (state: ReminderState) => ({
  timeLeftMs: state.timeLeftMs,
});

const Component = ({ message }: { message: LocalMessage }) => {
  // access the message reminder instance
  const reminder = useMessageReminder(message.id);
  const { timeLeftMs } =
    useStateStore(reminder?.state, reminderStateSelector) ?? {};
};

The timeLeftMs value is periodically updated. The update frequency increases as the deadline is approached and decreases as it is left behind (once per day, then last day every hour, last hour every minute and vice verse when increasing the distance from the deadline).

  1. Subscribe to reminders pagination

The pagination is handler by the RemindersPaginator. The SDK exposes a hook useQueryReminders that can be used to fetch paginated reminders and handle next page, etc making sure it reacts on update/deletion of reminders.

The items paginated are not Reminder instances but ReminderResponse objects retrieved from the server.

import { useCallback, useEffect } from "react";
import { FlatList, StyleSheet, Text, View } from "react-native";
import {
  useChatContext,
  useMessageReminder,
  useQueryReminders,
} from "stream-chat-react-native";
import { ReminderItem } from "./ReminderItem";

const renderItem = ({ item }: { item: ReminderResponse }) => (
  <ReminderItem {...item} />
);

const renderEmptyComponent = (
  <Text style={styles.emptyContainer}>No reminders available</Text>
);

const RemindersList = () => {
  const { client } = useChatContext();
  const { data, isLoading, loadNext } = useQueryReminders();

  useEffect(() => {
    client.reminders.paginator.filters = {};
    client.reminders.paginator.sort = { remind_at: 1 };
  }, [client.reminders]);

  const onRefresh = useCallback(async () => {
    await client.reminders.queryNextReminders();
  }, [client.reminders]);

  const renderFooter = useCallback(() => {
    if (isLoading) {
      return (
        <ActivityIndicator size={"small"} style={{ marginVertical: 16 }} />
      );
    }
  }, [isLoading]);

  return (
    <View style={{ flex: 1 }}>
      <FlatList
        contentContainerStyle={{ flexGrow: 1 }}
        data={items}
        keyExtractor={(item) => item.message.id}
        renderItem={renderItem}
        ListEmptyComponent={renderEmptyComponent}
        ListFooterComponent={renderFooter}
        onEndReached={loadNext}
      />
    </View>
  );
};

Message Reminder Configuration

It is possible to configure the reminder time offsets from which a user can select:

const minute = 60 * 1000;
client.reminders.updateConfig({
  scheduledOffsetsMs: [30 * minute, 60 * minute],
});

Also, it is possible to configure the time boundary, when the reminder notification will stop refreshing the information about the time elapsed since the deadline:

const day = 24 * 60 * 60 * 1000;
client.reminders.updateConfig({
  stopTimerRefreshBoundaryMs: day,
});

Reminder indicator on message

You can use the MessageHeader component to customize the header of the message to add the reminder indicator on top of the message. The MessageHeader component can be customized by passing a custom MessageHeader component to the Channel component:

Message Reminder Header

import {
  MessageFooterProps,
  Time,
  useMessageReminder,
  useStateStore,
  useTranslationContext,
} from "stream-chat-react-native";
import { ReminderState } from "stream-chat";
import { StyleSheet, Text, View } from "react-native";

const reminderStateSelector = (state: ReminderState) => ({
  timeLeftMs: state.timeLeftMs,
});

export const MessageReminderHeader = ({ message }: MessageFooterProps) => {
  const messageId = message?.id ?? "";
  const reminder = useMessageReminder(messageId);
  const { timeLeftMs } =
    useStateStore(reminder?.state, reminderStateSelector) ?? {};
  const { t } = useTranslationContext();

  const stopRefreshBoundaryMs = reminder?.timer.stopRefreshBoundaryMs;
  const stopRefreshTimeStamp =
    reminder?.remindAt && stopRefreshBoundaryMs
      ? reminder?.remindAt.getTime() + stopRefreshBoundaryMs
      : undefined;

  const isBehindRefreshBoundary =
    !!stopRefreshTimeStamp && new Date().getTime() > stopRefreshTimeStamp;

  if (!reminder) {
    return null;
  }

  // This is for "Saved for Later"
  if (!reminder.remindAt) {
    return (
      <View>
        <Text style={styles.headerTitle}>🔖 Saved for Later</Text>
      </View>
    );
  }

  if (reminder.remindAt && timeLeftMs !== null) {
    return (
      <View style={styles.headerContainer}>
        <Time height={16} width={16} />
        <Text style={styles.headerTitle}>
          {isBehindRefreshBoundary
            ? t("Due since {{ dueSince }}", {
                dueSince: t("timestamp/ReminderNotification", {
                  timestamp: reminder.remindAt,
                }),
              })
            : t("Due {{ timeLeft }}", {
                timeLeft: t("duration/Message reminder", {
                  milliseconds: timeLeftMs,
                }),
              })}
        </Text>
      </View>
    );
  }
};

const styles = StyleSheet.create({
  headerContainer: {
    flexDirection: "row",
    alignItems: "center",
  },
  headerTitle: {
    fontSize: 14,
    fontWeight: "500",
    marginLeft: 4,
  },
});
© Getstream.io, Inc. All Rights Reserved.