From f35c16096aa658a1589c97debbeebb0d574887cf Mon Sep 17 00:00:00 2001 From: Slava Bobik Date: Thu, 24 Sep 2026 16:21:18 +0200 Subject: [PATCH] feat(reminder): add expires_at to message reminders Reminders can now carry an expiry. Once it passes, the reminder is hidden from every read and stops counting against the per-user cap. Reminder, ReminderCreateRequest and ReminderUpdateRequest gain expiresAt; filtering on expires_at already works through the filter map. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/messages/message_reminders.md | 32 +++++++++ .../getstream/chat/java/models/Reminder.java | 15 ++++ .../chat/java/ReminderExpiresAtTest.java | 72 +++++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/test/java/io/getstream/chat/java/ReminderExpiresAtTest.java diff --git a/docs/messages/message_reminders.md b/docs/messages/message_reminders.md index f4080ece8..5130f6fde 100644 --- a/docs/messages/message_reminders.md +++ b/docs/messages/message_reminders.md @@ -61,6 +61,37 @@ MessageReminder updatedReminder = MessageReminder.update("message-id", "user-id" MessageReminder updatedReminder = MessageReminder.update("message-id", "user-id", null).request(); ``` +## Expiring a Message Reminder + +Set `expiresAt` to have a reminder remove itself. Once that time passes, the reminder no longer shows up in queries, updating or deleting it returns a 404, and it stops counting against the per-user reminder limit. No event is sent when a reminder expires, and creating a reminder on the same message again replaces the expired one. + +`expiresAt` must be at least one minute in the future and, when `remindAt` is set, later than `remindAt`. + +```java +// Backend SDK + +// Bookmark a message for 30 days +Date expiresAt = new Date(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000); + +Reminder.createReminder("message-id") + .userId("user-id") + .expiresAt(expiresAt) + .request(); +``` + +An update replaces both `remindAt` and `expiresAt`. A field left unset is cleared, so pass the current value of the one you want to keep. + +```java +// Backend SDK + +// Move the reminder time and keep the expiry +Reminder.updateReminder("message-id") + .userId("user-id") + .remindAt(newRemindAt) + .expiresAt(reminder.getExpiresAt()) + .request(); +``` + ## Deleting a Message Reminder You can delete a reminder for a message when it's no longer needed. @@ -96,6 +127,7 @@ You can filter the reminders based on different criteria: - `remind_at` - Filter by the reminder time. - `created_at` - Filter by the creation date. - `channel_cid` - Filter by the channel ID. +- `expires_at` - Filter by the expiry time. It cannot be used for sorting. The most common use case would be to filter by the reminder time. Like filtering overdue reminders, upcoming reminders, or reminders with no due date (saved for later). diff --git a/src/main/java/io/getstream/chat/java/models/Reminder.java b/src/main/java/io/getstream/chat/java/models/Reminder.java index 48b687c4e..c6428c0d8 100644 --- a/src/main/java/io/getstream/chat/java/models/Reminder.java +++ b/src/main/java/io/getstream/chat/java/models/Reminder.java @@ -57,6 +57,10 @@ public class Reminder { @JsonProperty("remind_at") private Date remindAt; + @Nullable + @JsonProperty("expires_at") + private Date expiresAt; + @Nullable @JsonProperty("created_at") private Date createdAt; @@ -92,6 +96,10 @@ public static class ReminderCreateRequestData { @JsonProperty("remind_at") private Date remindAt; + @Nullable + @JsonProperty("expires_at") + private Date expiresAt; + public static class ReminderCreateRequest extends StreamRequest { @NotNull private String messageId; @@ -121,6 +129,10 @@ public static class ReminderUpdateRequestData { @JsonProperty("remind_at") private Date remindAt; + @Nullable + @JsonProperty("expires_at") + private Date expiresAt; + public static class ReminderUpdateRequest extends StreamRequest { @NotNull private String messageId; @@ -243,6 +255,9 @@ public static ReminderCreateRequest createReminder(@NotNull String messageId) { /** * Updates a reminder for a message. * + *

The update replaces both {@code remind_at} and {@code expires_at}: a field left unset is + * cleared, so pass the current value to keep it. + * * @param messageId The ID of the message with the reminder * @return A request builder for updating a reminder */ diff --git a/src/test/java/io/getstream/chat/java/ReminderExpiresAtTest.java b/src/test/java/io/getstream/chat/java/ReminderExpiresAtTest.java new file mode 100644 index 000000000..d104457c4 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/ReminderExpiresAtTest.java @@ -0,0 +1,72 @@ +package io.getstream.chat.java; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import io.getstream.chat.java.models.Reminder; +import io.getstream.chat.java.models.Reminder.ReminderQueryResponse; +import java.util.Date; +import java.util.TimeZone; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ReminderExpiresAtTest { + + // Mirrors the visibility and date configuration of DefaultClient's mapper. + private static final ObjectMapper MAPPER = + new ObjectMapper() + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .setDateFormat( + new StdDateFormat() + .withColonInTimeZone(true) + .withTimeZone(TimeZone.getTimeZone("UTC"))); + + private static final Date EXPIRES_AT = new Date(1893456000000L); // 2030-01-01T00:00:00Z + + @DisplayName("Create sends expires_at when set") + @Test + void whenCreatingWithExpiresAt_thenBodyCarriesIt() throws Exception { + String body = + MAPPER.writeValueAsString( + Reminder.createReminder("msg").userId("user").expiresAt(EXPIRES_AT).internalBuild()); + + Assertions.assertTrue(body.contains("\"expires_at\":\"2030-01-01T00:00:00.000+00:00\""), body); + } + + @DisplayName("Update sends expires_at when set") + @Test + void whenUpdatingWithExpiresAt_thenBodyCarriesIt() throws Exception { + String body = + MAPPER.writeValueAsString( + Reminder.updateReminder("msg").userId("user").expiresAt(EXPIRES_AT).internalBuild()); + + Assertions.assertTrue(body.contains("\"expires_at\":\"2030-01-01T00:00:00.000+00:00\""), body); + } + + @DisplayName("Create without expires_at sends null, which means no expiry") + @Test + void whenCreatingWithoutExpiresAt_thenBodyCarriesNull() throws Exception { + String body = + MAPPER.writeValueAsString(Reminder.createReminder("msg").userId("user").internalBuild()); + + Assertions.assertTrue(body.contains("\"expires_at\":null"), body); + } + + @DisplayName("Responses read expires_at into the typed field") + @Test + void whenResponseHasExpiresAt_thenGetterReturnsIt() throws Exception { + ReminderQueryResponse response = + MAPPER.readValue( + "{\"reminders\":[{\"id\":\"r\",\"message_id\":\"msg\",\"user_id\":\"user\"," + + "\"channel_cid\":\"messaging:chan\",\"expires_at\":\"2030-01-01T00:00:00Z\"}]," + + "\"duration\":\"1ms\"}", + ReminderQueryResponse.class); + + Reminder reminder = response.getReminders().get(0); + Assertions.assertEquals(EXPIRES_AT, reminder.getExpiresAt()); + Assertions.assertFalse(reminder.getAdditionalFields().containsKey("expires_at")); + } +}