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

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

The user opens the menu on a message and picks Remind me. A submenu offers times: in 2 minutes, in 30 minutes, in an hour, in 2 hours, in 8 hours and in a day. They pick 30 minutes, and the message now shows "Reminder set · Due in 30 minutes". The time counts down as it approaches. A reminder more than 59 minutes away shows its calendar time instead, such as "Today at 15:00".

Save for later marks the message "Saved for later". The same menu then offers Remove reminder or Remove save for later. A message has one reminder at most, so while it has a scheduled reminder the menu hides Save for later, and while it's saved for later the menu hides Remind me.

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. MessageUI looks up the message's reminder with useMessageReminder(message.id) and renders ReminderNotification when there is one. The remindMe and saveForLater actions in the message menu create and delete reminders. Chat starts the manager's event subscriptions and timers when it mounts and stops them when it unmounts.
  • Layout. 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. The Remind me submenu uses it.

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

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. The SDK doesn't act on it: the message's label keeps counting, and nothing else happens. Alerting when a reminder is due adds an alert.

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, and the message menu shows Remind me and Save for later only when it's true.

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

The offered times

The Remind me submenu offers scheduledOffsetsMs, offsets from now in milliseconds:

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 label

Replace ReminderNotification through WithComponents. It receives the message's Reminder; read its state with useStateStore:

import type { Channel as ChannelType, ReminderState } from "stream-chat";
import {
  Channel,
  WithComponents,
  useStateStore,
  type ReminderNotificationProps,
} from "stream-chat-react";

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

const CustomReminderNotification = ({
  reminder,
}: ReminderNotificationProps) => {
  const { remindAt, timeLeftMs } =
    useStateStore(reminder?.state, reminderStateSelector) ?? {};

  if (!reminder) return null;
  if (remindAt == null) return <div className="reminder">🔖 Saved</div>;

  const minutes = Math.round((timeLeftMs ?? 0) / 60000);
  return (
    <div className="reminder">
      ⏰ {minutes > 0 ? `In ${minutes} min` : "Due now"}
    </div>
  );
};

export const ChannelWithCustomReminders = ({
  channel,
}: {
  channel: ChannelType;
}) => (
  <WithComponents
    overrides={{ ReminderNotification: CustomReminderNotification }}
  >
    <Channel channel={channel}>{/* MessageList, MessageComposer */}</Channel>
  </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 type { PaginatorState, ReminderResponseData } from "stream-chat";
import { useChatContext, useStateStore } from "stream-chat-react";

const reminderListSelector = ({
  hasMoreTail,
  isLoading,
  items,
}: PaginatorState<ReminderResponseData>) => ({ hasMoreTail, isLoading, items });

export const SavedForLaterList = () => {
  const { client } = useChatContext();
  const {
    hasMoreTail,
    isLoading,
    items = [],
  } = useStateStore(client.reminders.paginator.state, reminderListSelector);

  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 (
    <div className="saved-for-later">
      <ul>
        {items.map((reminder) => (
          <li key={reminder.message_id}>
            {reminder.message?.user?.name}: {reminder.message?.text}
          </li>
        ))}
      </ul>
      {hasMoreTail && (
        <button
          disabled={isLoading}
          onClick={() => void client.reminders.queryNextReminders()}
        >
          Load more
        </button>
      )}
    </div>
  );
};

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 }].

Alerting when a reminder is due

Listen for notification.reminder_due and publish a notification. Render this component once, inside Chat:

import { useEffect } from "react";
import { useChatContext } from "stream-chat-react";

export const ReminderDueAlerts = () => {
  const { client } = useChatContext();

  useEffect(() => {
    const { unsubscribe } = client.on("notification.reminder_due", (event) => {
      client.notifications.addInfo({
        message: `Reminder: ${event.reminder?.message?.text ?? "a saved message"}`,
        origin: { emitter: "ReminderDueAlerts" },
      });
    });
    return unsubscribe;
  }, [client]);

  return null;
};

Notifications explains where notifications show.

Reference