Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

Reminders and save for later

Reminders bring a message back to the user later. "Remind me" sets a time, and "Save for later" bookmarks the message with no time. Both are one feature in the API: a reminder with or without a remind_at.

What users get

A message with a reminder shows a line above it. With a time, it reads "Reminder set" and how long until it's due, such as "in 30 minutes", and the time counts down as it approaches. Once the time has passed, it reads "Reminder overdue" and how long ago it was due. A message saved for later reads "Saved For Later".

The default message menu has no reminder actions. Your app adds them, for example Remind me, Save for later and Remove reminder; Reminder actions builds them. A message has one reminder at most, so while it has one, the menu should offer only its removal.

Reminders belong to the user, not to the device: their reminders show on every device they're signed in on.

How it works across the layers

  • State. client.reminders is the reminder manager. It keeps one Reminder per message, keyed by the message id, and talks to the reminders API. Its paginator is the reminder list, the user's reminders across all channels.
  • UI. MessageHeader looks up the message's reminder with useMessageReminder(message.id). When there is one, it renders MessageReminderHeader for a reminder with a time, and MessageSavedForLaterHeader for one without. Chat starts the manager's event subscriptions and timers when it mounts and stops them when it unmounts.
  • Navigation. Not involved.

The manager's state store holds a Map from message id to Reminder. Several things fill it:

  • the messages a channel query returns carry their reminder field, so opening a channel brings in the reminders on its messages;
  • the reminder.created, reminder.updated and reminder.deleted events, so a reminder set on another device shows here too;
  • message.deleted removes the message's reminder, and message.undeleted restores it;
  • every page the reminder list loads;
  • your own calls: client.reminders.createReminder(), client.reminders.updateReminder(), client.reminders.upsertReminder() and client.reminders.deleteReminder().

None of these calls is optimistic. The state changes once the server answers:

// Remind me in an hour
await client.reminders.upsertReminder(
  { message_id: message.id },
  { remind_at: new Date(Date.now() + 60 * 60 * 1000) },
);

// Save for later: a reminder without a time
await client.reminders.createReminder({ message_id: message.id });

// Remove either kind
await client.reminders.deleteReminder(message.id);

client.reminders.upsertReminder() updates the message's reminder when it has one and creates it otherwise.

With offline support, reminders are stored with their messages in the local database.

The reminder list

client.reminders.paginator queries the user's reminders across channels. Its state store has items, isLoading, hasMoreTail and lastQueryError, like the other paginators in the state layer; How pagination works explains them. The items are reminder records from the server, each with its message and channel, rather than Reminder instances. Every loaded page also goes into the manager's state.

For a "Saved for later" screen, filter to reminders without a time:

  1. Set paginator.filters to { remind_at: { $exists: false } }. Setting filters or sort empties the list.
  2. Call client.reminders.queryNextReminders() for the first page, and again for each next page.
  3. Call ReminderPaginator.reload() to load the list from the start.

The paginator holds what its queries returned. A reminder created or deleted afterwards changes the manager's state, but not the paginator until it reloads. client.reminders.paginator is one list per client, so screens that need different filters take turns with it.

useQueryReminders() reads the paginator and also follows reminder events, so a list built on it changes when a reminder is created, updated or deleted. The SDK has no reminder list component. Your own reminder list builds one.

Reminder state and timers

Each Reminder has its own state store: the server fields (message_id, channel_cid, remind_at, created_at, updated_at, user_id, message and user) plus timeLeftMs. The timestamps are unix nanoseconds, as the API sends them; nsToDate(reminder.remindAt) turns one into a Date. timeLeftMs is in milliseconds and goes negative once the reminder is due. A reminder that is saved for later has remind_at: null and timeLeftMs: null.

A timer refreshes timeLeftMs, more often the closer the deadline is. Take a reminder set for three hours from now:

  1. It refreshes once an hour until the deadline is an hour away.
  2. Then once a minute, until the deadline is a minute away.
  3. Then once more, at the deadline itself.
  4. After the deadline, it slows down the same way: once a minute for the first hour, then once an hour for a day, then once a day.
  5. Two weeks after the deadline, the timer stops. stopTimerRefreshBoundaryMs sets that boundary.

When a reminder comes due, the server sends a notification.reminder_due event, and a push notification when your push setup includes that event. The SDK doesn't act on the event: the message's header keeps counting, and nothing else happens. See Push notifications.

Reminder push notifications need Push V3 on your app, as Message reminders says. Push is turned on per event type in the push templates, so turn it on for notification.reminder_due too, in the Dashboard or from your server. See Push templates.

Turning it on or off

Reminders are on when both of these allow them:

  • the channel type's user_message_reminders setting on the server, see Message reminders;
  • channel.userMessageReminders.enabled in the client configuration, which defaults to true.

A server "no" wins. The resolved value is channel.config.userMessageReminders.enabled. The SDK shows the header of any reminder it has, so check the value before you offer reminder actions, as the sample below does.

To turn reminders off in the client for every channel:

client.config.set({ channel: { userMessageReminders: { enabled: false } } });

Configuration explains client.config and how it combines with the server settings.

Customizing

Reminder actions

Add the actions through messageActions on Channel and Thread. The actions are built when the menu opens, so reading the reminder with client.reminders.getFromState() gets the current one:

import { useCallback } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import type { MessageActionsParams } from "stream-chat-react-native";
import {
  Bell,
  Bookmark,
  Channel,
  MessageComposer,
  MessageList,
  messageActions,
  useChatContext,
  useTheme,
} from "stream-chat-react-native";

const hour = 60 * 60 * 1000;

export const ChannelWithReminders = ({
  channel,
}: {
  channel: StreamChannel;
}) => {
  const { client } = useChatContext();
  const {
    theme: { semantics },
  } = useTheme();

  const withReminders = useCallback(
    (params: MessageActionsParams) => {
      const actions = messageActions(params);
      if (params.error || !channel.config.userMessageReminders.enabled) {
        return actions;
      }

      const messageId = params.message.id;
      const reminder = client.reminders.getFromState(messageId);
      const iconProps = {
        height: 20,
        stroke: semantics.textSecondary,
        width: 20,
      };
      const report = (error: unknown) => console.warn(error);

      if (reminder) {
        actions.push({
          action: () => {
            client.reminders.deleteReminder(messageId).catch(report);
          },
          actionType: "removeReminder",
          icon: <Bell {...iconProps} />,
          title:
            reminder.remindAt == null
              ? "Remove save for later"
              : "Remove reminder",
          type: "standard",
        });
        return actions;
      }

      actions.push(
        {
          action: () => {
            client.reminders
              .upsertReminder(
                { message_id: messageId },
                { remind_at: new Date(Date.now() + hour) },
              )
              .catch(report);
          },
          actionType: "remindMe",
          icon: <Bell {...iconProps} />,
          title: "Remind me in an hour",
          type: "standard",
        },
        {
          action: () => {
            client.reminders
              .createReminder({ message_id: messageId })
              .catch(report);
          },
          actionType: "saveForLater",
          icon: <Bookmark {...iconProps} />,
          title: "Save for later",
          type: "standard",
        },
      );
      return actions;
    },
    [channel, client, semantics],
  );

  return (
    <Channel channel={channel} messageActions={withReminders}>
      <MessageList />
      <MessageComposer />
    </Channel>
  );
};

To offer several times, open a picker of your own from the Remind me action. client.reminders.scheduledOffsetsMs holds the offsets to offer, in milliseconds from now: 2 minutes, 30 minutes, an hour, 2 hours, 8 hours and a day by default. A native Alert shows at most three buttons on Android, so use a bottom sheet or a menu for more.

The offered times

scheduledOffsetsMs and the timer boundary are set in client.config:

const minute = 60 * 1000;

client.config.set({
  client: {
    reminders: {
      scheduledOffsetsMs: [15 * minute, 60 * minute, 24 * 60 * minute],
      // Keep refreshing overdue reminders for one day instead of two weeks
      stopTimerRefreshBoundaryMs: 24 * 60 * minute,
    },
  },
});

client.reminders.updateConfig({ scheduledOffsetsMs }) changes the same settings on the manager directly.

The reminder headers

Replace MessageReminderHeader and MessageSavedForLaterHeader through WithComponents. Read the message's reminder with useMessageReminder(message.id), and its state with useStateStore:

import { Text } from "react-native";
import type { ReminderState } from "stream-chat";
import {
  useMessageContext,
  useMessageReminder,
  useStateStore,
  WithComponents,
} from "stream-chat-react-native";

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

const ReminderLabel = () => {
  const { message } = useMessageContext();
  const reminder = useMessageReminder(message.id);
  const { timeLeftMs } =
    useStateStore(reminder?.state, reminderStateSelector) ?? {};

  if (timeLeftMs == null) return null;

  const minutes = Math.round(timeLeftMs / 60000);
  return <Text>{minutes > 0 ? `Due in ${minutes} min` : "Due now"}</Text>;
};

const SavedLabel = () => <Text>Saved</Text>;

export const App = () => (
  <WithComponents
    overrides={{
      MessageReminderHeader: ReminderLabel,
      MessageSavedForLaterHeader: SavedLabel,
    }}
  >
    {/* the rest of the app */}
  </WithComponents>
);

To read a message's reminder anywhere inside Chat, call useMessageReminder(message.id). It returns the Reminder, or undefined when the message has none.

Your own reminder list

A "Saved for later" list, following the steps in The reminder list:

import { useEffect } from "react";
import { FlatList, Text } from "react-native";
import type { ReminderResponseData } from "stream-chat";
import { useChatContext, useQueryReminders } from "stream-chat-react-native";

const renderItem = ({ item }: { item: ReminderResponseData }) => (
  <Text numberOfLines={1}>
    {item.message?.user?.name}: {item.message?.text}
  </Text>
);

export const SavedForLaterList = () => {
  const { client } = useChatContext();
  const { data, loadNext } = useQueryReminders();

  useEffect(() => {
    // Setting the filter empties the list, so set it before the first query
    client.reminders.paginator.filters = { remind_at: { $exists: false } };
    void client.reminders.queryNextReminders();
  }, [client]);

  return (
    <FlatList
      data={data}
      keyExtractor={(item) => item.message_id}
      onEndReached={() => void loadNext()}
      renderItem={renderItem}
    />
  );
};

useQueryReminders() adds a reminder created afterwards to data whatever the filter, so a saved-for-later list also shows a new reminder with a time until it reloads. For a list of scheduled reminders, use { remind_at: { $exists: true } } and sort by the deadline with client.reminders.paginator.sort = [{ field: "remind_at", direction: 1 }]. The SampleApp's Reminders tab (examples/SampleApp/src/components/Reminders/RemindersList.tsx) switches between several filters.

Reference