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.
client.reminders.state holds a Map from message id to Reminder. Channel queries, reminder and message events, the reminder list and your own calls fill it; Events lists what changes it.
Save for later is a reminder without a remind_at. The Remind me submenu calls ReminderManager.upsertReminder(), and Save for later calls ReminderManager.createReminder(). Neither is optimistic: the state changes once the server answers.
// Remind me in an hour, as the Remind me submenu does
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 });Methods lists the other calls, including ReminderManager.deleteReminder().
The reminder list
client.reminders.paginator loads the user's reminders across channels, one paginator per client. For a "Saved for later" screen, filter to reminders without a time:
- Set
client.reminders.paginator.filtersto{ remind_at: { $exists: false } }. Settingfiltersorsortempties the list. - Call
ReminderManager.queryNextReminders()for the first page, and again for each next page. - Call
ReminderPaginator.reload()to load the list from the start.
The reminder list explains what its items hold and when it changes. The SDK has no reminder list component; Your own reminder list builds one.
Reminder state and timers
Each Reminder has its own state store with the server fields and timeLeftMs, which a timer refreshes more often as the deadline gets closer. Each reminder's state lists the fields, and Timers the refresh schedule. When a reminder comes due, the SDK doesn't alert the user; 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 UI 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.