From 9736eebdda50646d52e5b10f85c7fcc82389f9ea Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 9 Sep 2026 14:40:48 +0200 Subject: [PATCH 1/2] feat: add skip_truncate to channel delete Soft deleting a channel truncates it, so a channel recreated with the same id comes back empty. ChannelDeleteRequest and ChannelDeleteManyRequest take setSkipTruncate, sent as a query param on the single delete and a body field on the batch delete, both omitted when unset. Co-Authored-By: Claude Opus 5 --- docs/channels/channel_management/deleting.md | 11 +++ .../getstream/chat/java/models/Channel.java | 27 +++++++- .../chat/java/services/ChannelService.java | 4 +- .../java/ChannelDeleteSkipTruncateTest.java | 68 +++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java diff --git a/docs/channels/channel_management/deleting.md b/docs/channels/channel_management/deleting.md index 4c06c1fc5..059a98fe5 100644 --- a/docs/channels/channel_management/deleting.md +++ b/docs/channels/channel_management/deleting.md @@ -16,6 +16,17 @@ Channel.delete( > If you recreate this channel, it will show up empty. Recovering old messages is not supported. Use the disable method if you want a reversible change. +### Keeping the messages + +Call `setSkipTruncate(true)` to keep the messages of a soft deleted channel, so recreating the channel with the same id restores its history. It cannot be combined with a hard delete, and only distinct channels are eligible. + +```java +Channel.delete("messaging", channelId).setSkipTruncate(true).request(); + +// same option on the batch endpoint +Channel.deleteMany(Arrays.asList(cid1, cid2)).setSkipTruncate(true).request(); +``` + ## Deleting Many Channels You can delete up to 100 channels and optionally all of their messages using this method. This can be a large amount of data to delete, so this endpoint processes asynchronously, meaning responses contain a `task ID` which can be polled using the [getTask endpoint](/chat/docs/java#tasks-gettask) to check status of the deletions. Channels will be soft-deleted immediately so that channels no longer return from queries, but permanently deleting the channel and deleting messages takes longer to process. 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 cf2873226..f6ab4d29f 100644 --- a/src/main/java/io/getstream/chat/java/models/Channel.java +++ b/src/main/java/io/getstream/chat/java/models/Channel.java @@ -792,9 +792,22 @@ public static class ChannelDeleteRequest extends StreamRequest generateCall(Client client) { - return client.create(ChannelService.class).delete(this.channelType, this.channelId); + return client + .create(ChannelService.class) + .delete(this.channelType, this.channelId, this.skipTruncate); } } @@ -811,11 +824,23 @@ public static class ChannelDeleteManyRequest extends StreamRequest generateCall(Client svcFactory) throws StreamException { diff --git a/src/main/java/io/getstream/chat/java/services/ChannelService.java b/src/main/java/io/getstream/chat/java/services/ChannelService.java index f71803836..20470b88d 100644 --- a/src/main/java/io/getstream/chat/java/services/ChannelService.java +++ b/src/main/java/io/getstream/chat/java/services/ChannelService.java @@ -37,7 +37,9 @@ Call getChannel( @DELETE("channels/{type}/{id}") Call delete( - @NotNull @Path("type") String channelType, @NotNull @Path("id") String channelId); + @NotNull @Path("type") String channelType, + @NotNull @Path("id") String channelId, + @Nullable @Query("skip_truncate") Boolean skipTruncate); @POST("channels/delete") Call deleteMany( diff --git a/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java b/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java new file mode 100644 index 000000000..deba333a6 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java @@ -0,0 +1,68 @@ +package io.getstream.chat.java; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.getstream.chat.java.models.Channel; +import io.getstream.chat.java.services.ChannelService; +import java.util.Arrays; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; + +public class ChannelDeleteSkipTruncateTest { + + // Mirrors the visibility configuration of DefaultClient's mapper. + private static final ObjectMapper MAPPER = + new ObjectMapper() + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + private static ChannelService service() { + return new Retrofit.Builder() + .baseUrl("https://chat.example.com/") + .addConverterFactory(JacksonConverterFactory.create(MAPPER)) + .build() + .create(ChannelService.class); + } + + @DisplayName("Delete sends skip_truncate as a query param when set") + @Test + void whenSkipTruncateSet_thenQueryParamIsSent() { + String url = service().delete("messaging", "chan", true).request().url().toString(); + + Assertions.assertTrue(url.contains("skip_truncate=true"), url); + } + + @DisplayName("Delete omits skip_truncate when unset") + @Test + void whenSkipTruncateUnset_thenQueryParamIsOmitted() { + String url = service().delete("messaging", "chan", null).request().url().toString(); + + Assertions.assertFalse(url.contains("skip_truncate"), url); + } + + @DisplayName("Delete request carries the flag to the service call") + @Test + void whenSettingSkipTruncateOnRequest_thenFlagIsKept() { + Assertions.assertEquals( + true, Channel.delete("messaging", "chan").setSkipTruncate(true).getSkipTruncate()); + Assertions.assertNull(Channel.delete("messaging", "chan").getSkipTruncate()); + } + + @DisplayName("Delete many serializes skip_truncate only when set") + @Test + void whenSettingSkipTruncateOnDeleteMany_thenBodyCarriesIt() throws Exception { + Assertions.assertFalse( + MAPPER + .writeValueAsString(Channel.deleteMany(Arrays.asList("messaging:chan"))) + .contains("skip_truncate")); + Assertions.assertTrue( + MAPPER + .writeValueAsString( + Channel.deleteMany(Arrays.asList("messaging:chan")).setSkipTruncate(true)) + .contains("\"skip_truncate\":true")); + } +} From 25f0210216224ab756b29e31f967f8661513998b Mon Sep 17 00:00:00 2001 From: Yun Wang Date: Wed, 9 Sep 2026 14:54:26 +0200 Subject: [PATCH 2/2] fix: keep the two argument delete on ChannelService Adding skip_truncate to the existing method changed a public interface signature. Keep it and add a three argument overload beside it, so callers of ChannelService.delete(type, id) still compile. Co-Authored-By: Claude Opus 5 --- .../io/getstream/chat/java/services/ChannelService.java | 4 ++++ .../chat/java/ChannelDeleteSkipTruncateTest.java | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/io/getstream/chat/java/services/ChannelService.java b/src/main/java/io/getstream/chat/java/services/ChannelService.java index 20470b88d..488558e99 100644 --- a/src/main/java/io/getstream/chat/java/services/ChannelService.java +++ b/src/main/java/io/getstream/chat/java/services/ChannelService.java @@ -35,6 +35,10 @@ Call getChannel( @NotNull @Path("id") String channelId, @NotNull @ToJson @Query("payload") GetChannelRequestData getChannelRequestData); + @DELETE("channels/{type}/{id}") + Call delete( + @NotNull @Path("type") String channelType, @NotNull @Path("id") String channelId); + @DELETE("channels/{type}/{id}") Call delete( @NotNull @Path("type") String channelType, diff --git a/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java b/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java index deba333a6..f8512c03a 100644 --- a/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java +++ b/src/test/java/io/getstream/chat/java/ChannelDeleteSkipTruncateTest.java @@ -44,6 +44,14 @@ void whenSkipTruncateUnset_thenQueryParamIsOmitted() { Assertions.assertFalse(url.contains("skip_truncate"), url); } + @DisplayName("The two argument delete overload is still callable") + @Test + void whenCallingTwoArgumentDelete_thenNoQueryParamIsSent() { + String url = service().delete("messaging", "chan").request().url().toString(); + + Assertions.assertFalse(url.contains("skip_truncate"), url); + } + @DisplayName("Delete request carries the flag to the service call") @Test void whenSettingSkipTruncateOnRequest_thenFlagIsKept() {