Reminder manager
client.reminders is the reminder manager, a ReminderManager from stream-chat. It keeps one Reminder per message, talks to the reminders API, and holds the user's reminder list. A reminder with a remind_at time is "remind me"; one without is "save for later". This page lists its API.
How reminders work end to end, with a "Saved for later" list and an alert when a reminder is due, is on Reminders and save for later.
Getting the manager
The client creates the manager when it's constructed, so there is one per client: client.reminders. Don't construct a ReminderManager yourself.
The manager follows events only while its subscriptions run:
| Method | What it does |
|---|---|
registerSubscriptions() |
Starts applying the reminder and message events listed in Events. A second call does nothing. |
unregisterSubscriptions() |
Stops applying them. |
initTimers() |
Starts the timer of every reminder in the state. |
clearTimers() |
Stops the timer of every reminder in the state. |
The UI layer starts the subscriptions and the timers for you, and stops them when it's done. Code that uses the client without a UI layer calls these itself.
State
The manager's state
client.reminders.state is a store with one field:
| Field | Type | What it holds |
|---|---|---|
reminders |
Map<string, Reminder> |
One Reminder per message, keyed by the message id. A new Map on every change. |
The map changes when a reminder is added or removed. A change to an existing reminder, such as a new remind_at, is published on that reminder's own state, not here. So following one message's reminder takes two subscriptions: one to find the Reminder, one to follow it.
const messageId = "message-1";
const unsubscribe = client.reminders.state.subscribeWithSelector(
({ reminders }) => ({ reminder: reminders.get(messageId) }),
({ reminder }) => {
const remindAt = reminder?.remindAt;
console.log(
remindAt != null
? `Due ${nsToDate(remindAt).toLocaleString()}`
: "No time set",
);
},
);The getters of client.reminders return the value at the moment you read them, and don't tell you when it changes:
| Getter or method | Returns |
|---|---|
reminders |
The current Map of reminders. |
getFromState(messageId) |
The message's Reminder, or undefined. |
Each reminder's state
A Reminder has its own state store, of type ReminderState. The timestamps are unix nanoseconds, as the API sends them: convert them with nsToDate() or nsToMs(), as Data conventions explains.
| Field | Type | What it holds |
|---|---|---|
message_id |
string |
The message the reminder belongs to. It's also the reminder's id. |
channel_cid |
string |
The channel of the message. |
remind_at |
TimestampNS or null |
When the reminder is due. null for a message saved for later. |
timeLeftMs |
number or null |
Milliseconds until remind_at, negative once it's due. null without a remind_at. The timer refreshes it. |
message |
MessageResponse or null |
The message, when the server sent it with the reminder. |
user_id |
string |
The user the reminder belongs to. |
user |
UserResponse or null |
That user, when the server sent it. |
created_at |
TimestampNS |
When the reminder was created. |
updated_at |
TimestampNS |
When it last changed. |
A Reminder also has:
| Member | What it does |
|---|---|
id |
Getter: the message id. |
remindAt |
Getter: remind_at. |
timeLeftMs |
Getter: timeLeftMs. |
timer |
The reminder's ReminderTimer. See Timers. |
setState(data: ReminderResponseData) |
Replaces the state with server data, then starts the timer, or stops it when there is no remind_at. |
refreshTimeLeft() |
Recomputes timeLeftMs from the clock. |
initTimer(), clearTimer() |
Starts or stops the reminder's timer. |
timeLeftMs(remindAt), exported from stream-chat, computes the same value as the state field for any nanosecond timestamp.
The reminder list
client.reminders.paginator is a ReminderPaginator: the current user's reminders across all channels, loaded page by page. It's one paginator per client. Its state has the fields every paginator in the state layer has (items, isLoading, hasMoreHead, hasMoreTail, lastQueryError), and How pagination works explains them. The base paginator API is on Channel manager and ChannelPaginator.
| Member | What it does |
|---|---|
items |
Reminder records from the server (ReminderResponseData), each with its message and channel. They aren't Reminder instances. |
filters |
Getter and setter. Filters on channel_cid, message_id, created_at, remind_at and expires_at. { remind_at: { $exists: false } } keeps the saved-for-later ones. Setting it empties the list. |
sort |
Getter and setter, such as [{ field: "remind_at", direction: 1 }]. Setting it empties the list. Unset, the list is ordered by created_at. |
pageSize |
Reminders per page, 10 by default. |
reload() |
Empties the list and loads the first page. |
While the manager's subscriptions run, every page the list loads also goes into the manager's state, so each loaded reminder gets its Reminder. The list itself holds what its queries returned: a reminder created or deleted later changes the manager's state, but not the list until it reloads.
Methods
None of these is optimistic. The manager's state changes once the server answers.
| Method | Returns | What it does |
|---|---|---|
createReminder({ message_id }, { remind_at?, expires_at? }?) |
Promise<Reminder | undefined> |
Creates the message's reminder and adds it to the state. Without remind_at, the message is saved for later. |
updateReminder({ message_id }, { remind_at?, expires_at? }?) |
Promise<Reminder | undefined> |
Changes the message's reminder and updates the state. |
upsertReminder({ message_id }, { remind_at?, expires_at? }?) |
Promise<Reminder | undefined> |
Updates the reminder when the state has one for the message, and creates it otherwise. When the server answers that the reminder already exists, or that it doesn't, it makes the other call. |
deleteReminder(messageId: string) |
Promise<void> |
Deletes the message's reminder, then removes it from the state and stops its timer. |
queryNextReminders() |
Promise<void> |
Loads the next page of paginator, or the first page when nothing is loaded. |
queryPreviousReminders() |
Promise<void> |
Loads a page at the other end of paginator. |
remind_at and expires_at are Date objects in a request, and the methods take an optional third argument with request options. They resolve to the Reminder that was already in the state. For a message that had no reminder yet, they add a new Reminder and resolve to undefined: read it with ReminderManager.getFromState(messageId) afterwards.
// Remind me in an hour
await client.reminders.upsertReminder(
{ message_id: message.id },
{ remind_at: new Date(Date.now() + 60 * 60 * 1000) },
);
const reminder = client.reminders.getFromState(message.id);These methods write the state without a request. The manager uses them itself, and they don't change anything on the server:
| Method | What it does |
|---|---|
upsertToState({ data, overwrite? }) |
Adds a Reminder for data.message_id, or, with overwrite (true by default), updates the existing one. |
removeFromState(messageId) |
Removes the message's reminder and stops its timer. |
hydrateState(messages) |
Adds the reminder of each message that carries one. |
Configuration
The manager has the config, configState and ReminderManager.updateConfig() that every configurable object has, and ReminderManager.initializeConfig(), which the client calls to rebuild the settings from client.config. In client.config, the settings are the reminders group of the client key:
| Setting | Default | What it does |
|---|---|---|
scheduledOffsetsMs |
2 minutes, 30 minutes, 1, 2 and 8 hours, 1 day | The "remind me in…" choices a UI offers, as offsets from now in milliseconds. |
stopTimerRefreshBoundaryMs |
2 weeks | How long after its deadline a reminder's timer keeps refreshing timeLeftMs. |
The getters scheduledOffsetsMs and stopTimerRefreshBoundaryMs read the current values. A change to stopTimerRefreshBoundaryMs reaches the timers that are running. Configuration tree lists both, and Configuration explains how to set them.
Whether reminders are offered in a channel is a channel setting: channel.config.userMessageReminders.enabled, which the channel type's user_message_reminders can turn off.
Events
While its subscriptions run, the manager applies these events:
| Event | What changes |
|---|---|
reminder.created |
The reminder is added, or updated if it's in the state already. |
reminder.updated |
The reminder is updated, or added. |
reminder.deleted |
The reminder is removed. |
message.deleted |
The message's reminder is removed. |
message.undeleted |
The message's reminder is added back, when the message carries one. |
notification.reminder_due |
Nothing. The manager doesn't act on it. |
ReminderManager.isReminderWsEventPayload(event) tells whether an event is a reminder.* or notification.reminder_due event that carries a reminder.
Reminders also arrive with messages. A channel query and client.queryChannels() add the reminder of every message they load, with or without subscriptions.
Timers
Each Reminder with a remind_at has a timer that refreshes timeLeftMs in its state. The timer starts when the reminder is created or gets a remind_at, and stops when the reminder is removed or loses it. It refreshes more often the closer the deadline is, before and after it:
| Time to or from the deadline | The timer fires |
|---|---|
| More than a day | Once a day. |
| Between an hour and a day | Once an hour. |
| Between a minute and an hour | Once a minute. |
| Less than a minute | Once, when that time has passed. Before the deadline, that's the deadline itself. |
Once a reminder is overdue by stopTimerRefreshBoundaryMs, two weeks by default, its timer stops and timeLeftMs stays as it was last set.
A tick changes only that reminder's timeLeftMs. The manager's reminders map doesn't change, and nothing is published elsewhere. When the reminder comes due, the server sends notification.reminder_due, which the manager ignores. To alert the user, listen for that event, as Alerting when a reminder is due shows.
reminder.timer is a ReminderTimer:
| Member | What it is |
|---|---|
stopRefreshBoundaryMs |
The boundary this timer uses. The manager sets it from its configuration. |
timeout |
The pending timeout, or null when the timer isn't running. |
getRefreshIntervalLength() |
The delay until the next tick, from the table above, or null without a remind_at. |
init() |
Schedules the next tick. Does nothing once the boundary has passed. |
clear() |
Cancels the pending tick. |
See also
- Reminders and save for later: the feature end to end.
- How pagination works: the paginator state the reminder paginator shares.
- Reactive state: subscribing to stores.
- Message reminders in the JavaScript client: the reminders API and the server setting.