From 1f850f3aad772fa3000cd90e68fb1bfd357656be Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 12 Aug 2026 16:40:06 +0200 Subject: [PATCH 1/3] feat(channel): page getChannel messages by message ID Adds messagesIdLt, messagesIdLte, messagesIdGt, messagesIdGte and messagesIdAround to the getChannel request. They page the messages of the channel by message ID, so a caller no longer needs getOrCreate for that. The cursors need state(true), and a message ID the channel does not hold yields a 404. --- DOCS.md | 10 ++++++++ .../getstream/chat/java/models/Channel.java | 23 ++++++++++++++++++- .../getstream/chat/java/GetChannelTest.java | 22 ++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/DOCS.md b/DOCS.md index f10f4a46..65659725 100644 --- a/DOCS.md +++ b/DOCS.md @@ -378,6 +378,16 @@ Channel.getChannel("messaging", "travel") .request(); ``` +Message pagination. The cursors are `messagesIdLt`, `messagesIdLte`, `messagesIdGt`, `messagesIdGte` and `messagesIdAround`. They need `state(true)`, and a message ID the channel does not hold yields a 404. + +```java +Channel.getChannel("messaging", "travel") + .state(true) + .messagesLimit(20) + .messagesIdLt(oldestLoadedMessageId) + .request(); +``` + **Get or create channel (type,id)** Standard diff --git a/src/main/java/io/getstream/chat/java/models/Channel.java b/src/main/java/io/getstream/chat/java/models/Channel.java index cdc99b0d..aec20893 100644 --- a/src/main/java/io/getstream/chat/java/models/Channel.java +++ b/src/main/java/io/getstream/chat/java/models/Channel.java @@ -575,6 +575,26 @@ public static class GetChannelRequestData { @JsonProperty("watchers_limit") private Integer watchersLimit; + @Nullable + @JsonProperty("messages_id_lt") + private String messagesIdLt; + + @Nullable + @JsonProperty("messages_id_lte") + private String messagesIdLte; + + @Nullable + @JsonProperty("messages_id_gt") + private String messagesIdGt; + + @Nullable + @JsonProperty("messages_id_gte") + private String messagesIdGte; + + @Nullable + @JsonProperty("messages_id_around") + private String messagesIdAround; + public static class GetChannelRequest extends StreamRequest { @NotNull private String channelType; @@ -1688,7 +1708,8 @@ public static ChannelGetRequest getOrCreate(@NotNull String type) { /** * Creates a get channel request. The channel is never created: a missing, deleted or disabled - * channel yields a 404, so this request doubles as an existence check. + * channel yields a 404, so this request doubles as an existence check. The messages of the + * channel are paged with the messagesId* parameters, which need state to be true. * * @param type the channel type * @param id the channel id diff --git a/src/test/java/io/getstream/chat/java/GetChannelTest.java b/src/test/java/io/getstream/chat/java/GetChannelTest.java index 100e32a5..ee411093 100644 --- a/src/test/java/io/getstream/chat/java/GetChannelTest.java +++ b/src/test/java/io/getstream/chat/java/GetChannelTest.java @@ -27,6 +27,28 @@ void whenBuildingGetChannelRequest_thenOptionsGoIntoPayloadQueryParameter() { Assertions.assertTrue(payload.contains("\"messages_limit\":5"), payload); } + @Test + @DisplayName("Get channel sends its message cursors in the payload query parameter") + void whenBuildingGetChannelRequestWithCursors_thenCursorsGoIntoPayloadQueryParameter() { + var data = + Channel.getChannel("messaging", "abc") + .state(true) + .messagesLimit(20) + .messagesIdLt("message-1") + .messagesIdGte("message-2") + .messagesIdAround("message-3") + .internalBuild(); + + var request = + client().create(ChannelService.class).getChannel("messaging", "abc", data).request(); + + var payload = request.url().queryParameter("payload"); + Assertions.assertNotNull(payload); + Assertions.assertTrue(payload.contains("\"messages_id_lt\":\"message-1\""), payload); + Assertions.assertTrue(payload.contains("\"messages_id_gte\":\"message-2\""), payload); + Assertions.assertTrue(payload.contains("\"messages_id_around\":\"message-3\""), payload); + } + @Test @DisplayName("Get channel sends a payload query parameter even without options") void whenBuildingGetChannelRequestWithoutOptions_thenPayloadQueryParameterIsStillSent() { From c6e20bb5aa92daade46fa1b030f272999775f0c1 Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 12 Aug 2026 19:03:39 +0200 Subject: [PATCH 2/3] docs: a getChannel message cursor that does not exist yields a 400 --- DOCS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCS.md b/DOCS.md index 65659725..f20a843f 100644 --- a/DOCS.md +++ b/DOCS.md @@ -378,7 +378,7 @@ Channel.getChannel("messaging", "travel") .request(); ``` -Message pagination. The cursors are `messagesIdLt`, `messagesIdLte`, `messagesIdGt`, `messagesIdGte` and `messagesIdAround`. They need `state(true)`, and a message ID the channel does not hold yields a 404. +Message pagination. The cursors are `messagesIdLt`, `messagesIdLte`, `messagesIdGt`, `messagesIdGte` and `messagesIdAround`. They need `state(true)`, and a message ID the channel does not hold yields a 400. ```java Channel.getChannel("messaging", "travel") From 9c76ad05204c6c85a9039b1e57c7e037182268ba Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 12 Aug 2026 19:24:52 +0200 Subject: [PATCH 3/3] test(channel): cover getChannel message cursors against the API Pages a three-message channel with messagesIdLt and messagesIdGt, and asserts that a cursor the channel does not hold fails with 400. --- .../io/getstream/chat/java/ChannelTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/test/java/io/getstream/chat/java/ChannelTest.java b/src/test/java/io/getstream/chat/java/ChannelTest.java index a468c145..bf9e232a 100644 --- a/src/test/java/io/getstream/chat/java/ChannelTest.java +++ b/src/test/java/io/getstream/chat/java/ChannelTest.java @@ -4,6 +4,7 @@ import io.getstream.chat.java.models.Channel; import io.getstream.chat.java.models.Channel.*; import io.getstream.chat.java.models.DeleteStrategy; +import io.getstream.chat.java.models.Message; import io.getstream.chat.java.models.Message.MessageRequestObject; import io.getstream.chat.java.models.Sort; import io.getstream.chat.java.models.Sort.Direction; @@ -51,6 +52,76 @@ void whenGettingExistingChannel_thenReturnsChannel() { Assertions.assertEquals(testChannel.getId(), response.getChannel().getId()); } + @DisplayName("Can page the messages of a channel by message ID") + @Test + void whenGettingChannelWithMessageCursor_thenPagesMessages() { + var channelId = RandomStringUtils.randomAlphabetic(12); + Assertions.assertDoesNotThrow( + () -> + Channel.getOrCreate(testChannel.getType(), channelId) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members(buildChannelMembersList()) + .build()) + .request()); + + var first = sendMessageToChannel(channelId, "first"); + var second = sendMessageToChannel(channelId, "second"); + var third = sendMessageToChannel(channelId, "third"); + + var older = + Assertions.assertDoesNotThrow( + () -> + Channel.getChannel(testChannel.getType(), channelId) + .state(true) + .messagesIdLt(third) + .request()); + Assertions.assertEquals(Arrays.asList(first, second), messageIds(older)); + + var newer = + Assertions.assertDoesNotThrow( + () -> + Channel.getChannel(testChannel.getType(), channelId) + .state(true) + .messagesIdGt(first) + .request()); + Assertions.assertEquals(Arrays.asList(second, third), messageIds(newer)); + } + + @DisplayName("Get channel rejects a message cursor the channel does not hold") + @Test + void whenGettingChannelWithUnknownMessageCursor_thenBadRequest() { + var exception = + Assertions.assertThrows( + StreamException.class, + () -> + Channel.getChannel(testChannel.getType(), testChannel.getId()) + .state(true) + .messagesIdLt(RandomStringUtils.randomAlphabetic(12)) + .request()); + Assertions.assertEquals(400, exception.getResponseData().getStatusCode()); + } + + private String sendMessageToChannel(String channelId, String text) { + var message = + Assertions.assertDoesNotThrow( + () -> + Message.send(testChannel.getType(), channelId) + .message( + MessageRequestObject.builder() + .text(text) + .userId(testUserRequestObject.getId()) + .build()) + .request() + .getMessage()); + return message.getId(); + } + + private List messageIds(ChannelGetResponse response) { + return response.getMessages().stream().map(Message::getId).collect(Collectors.toList()); + } + @DisplayName("Get channel does not create a missing channel") @Test void whenGettingMissingChannel_thenNotFound() {