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.remindersis the reminder manager. It keeps oneReminderper message, keyed by the message id, and talks to the reminders API. Itspaginatoris the reminder list, the user's reminders across all channels. - UI.
MessageUIlooks up the message's reminder withuseMessageReminder(message.id)and rendersReminderNotificationwhen there is one. TheremindMeandsaveForLateractions in the message menu create and delete reminders.Chatstarts 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
reminderfield, so opening a channel brings in the reminders on its messages; - the
reminder.created,reminder.updatedandreminder.deletedevents, so a reminder set on another device shows here too; message.deletedremoves the message's reminder, andmessage.undeletedrestores it;- every page the reminder list loads;
- your own calls:
client.reminders.createReminder(),client.reminders.updateReminder(),client.reminders.upsertReminder()andclient.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:
- Set
paginator.filtersto{ remind_at: { $exists: false } }. Settingfiltersorsortempties the list. - Call
client.reminders.queryNextReminders()for the first page, and again for each next page. - 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:
- It refreshes once an hour until the deadline is an hour away.
- Then once a minute, until the deadline is a minute away.
- Then once more, at the deadline itself.
- 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.
- Two weeks after the deadline, the timer stops.
stopTimerRefreshBoundaryMssets 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_reminderssetting on the server, see Message reminders; channel.userMessageReminders.enabledin the client configuration, which defaults totrue.
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
- Reminder manager:
client.reminders, its state, methods and timers. - Reminder hooks:
useMessageReminder. - ComponentContext: the
ReminderNotificationoverride key. - Message actions: the Remind me and Save for later actions.
- Message reminders in the JavaScript client: the reminders API and the server setting.