diff --git a/src/main/java/io/getstream/chat/java/models/Thread.java b/src/main/java/io/getstream/chat/java/models/Thread.java new file mode 100644 index 000000000..fd40bedce --- /dev/null +++ b/src/main/java/io/getstream/chat/java/models/Thread.java @@ -0,0 +1,226 @@ +package io.getstream.chat.java.models; + +import com.fasterxml.jackson.annotation.*; +import io.getstream.chat.java.exceptions.StreamException; +import io.getstream.chat.java.models.framework.StreamRequest; +import io.getstream.chat.java.models.framework.StreamResponseObject; +import io.getstream.chat.java.services.ThreadService; +import io.getstream.chat.java.services.framework.Client; +import java.util.Date; +import java.util.List; +import java.util.Map; +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import retrofit2.Call; + +/** Represents thread functionality in Stream Chat. */ +@Data +public class Thread { + /** A thread participant. */ + @Data + @NoArgsConstructor + public static class ThreadParticipant { + @Nullable + @JsonProperty("app_pk") + private Integer appPk; + + @Nullable + @JsonProperty("channel_cid") + private String channelCid; + + @Nullable + @JsonProperty("last_thread_message_at") + private Date lastThreadMessageAt; + + @Nullable + @JsonProperty("thread_id") + private String threadId; + + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User user; + + @Nullable + @JsonProperty("created_at") + private Date createdAt; + + @Nullable + @JsonProperty("left_thread_at") + private Date leftThreadAt; + + @Nullable + @JsonProperty("last_read_at") + private Date lastReadAt; + + @Nullable + @JsonProperty("custom") + private Map custom; + } + + /** A thread object containing thread data and metadata. */ + @Data + @NoArgsConstructor + public static class ThreadObject { + @Nullable + @JsonProperty("app_pk") + private Integer appPk; + + @NotNull + @JsonProperty("channel_cid") + private String channelCid; + + @Nullable + @JsonProperty("channel") + private Channel channel; + + @NotNull + @JsonProperty("parent_message_id") + private String parentMessageId; + + @Nullable + @JsonProperty("parent_message") + private Message parentMessage; + + @NotNull + @JsonProperty("created_by_user_id") + private String createdByUserId; + + @Nullable + @JsonProperty("created_by") + private User createdBy; + + @Nullable + @JsonProperty("reply_count") + private Integer replyCount; + + @Nullable + @JsonProperty("participant_count") + private Integer participantCount; + + @Nullable + @JsonProperty("active_participant_count") + private Integer activeParticipantCount; + + @Nullable + @JsonProperty("thread_participants") + private List participants; + + @Nullable + @JsonProperty("last_message_at") + private Date lastMessageAt; + + @NotNull + @JsonProperty("created_at") + private Date createdAt; + + @NotNull + @JsonProperty("updated_at") + private Date updatedAt; + + @Nullable + @JsonProperty("deleted_at") + private Date deletedAt; + + @NotNull + @JsonProperty("title") + private String title; + + @Nullable + @JsonProperty("custom") + private Map custom; + + @Nullable + @JsonProperty("latest_replies") + private List latestReplies; + + @Nullable + @JsonProperty("read") + private List read; + + @Nullable + @JsonProperty("draft") + private Draft.DraftObject draft; + } + + /** Response for querying threads. */ + @Data + @NoArgsConstructor + @EqualsAndHashCode(callSuper = true) + public static class QueryThreadsResponse extends StreamResponseObject { + @NotNull + @JsonProperty("threads") + private List threads; + + @Nullable + @JsonProperty("next") + private String next; + + @Nullable + @JsonProperty("prev") + private String prev; + } + + /** Request data for querying threads. */ + @Builder( + builderClassName = "QueryThreadsRequest", + builderMethodName = "", + buildMethodName = "internalBuild") + public static class QueryThreadsRequestData { + @Nullable + @JsonProperty("filter") + private Map filter; + + @Singular + @Nullable + @JsonProperty("sort") + private List sorts; + + @Nullable + @JsonProperty("watch") + private Boolean watch; + + @Nullable + @JsonProperty("user_id") + private String userId; + + @Nullable + @JsonProperty("user") + private User.UserRequestObject user; + + @Nullable + @JsonProperty("limit") + private Integer limit; + + @Nullable + @JsonProperty("next") + private String next; + + @Nullable + @JsonProperty("prev") + private String prev; + + public static class QueryThreadsRequest extends StreamRequest { + public QueryThreadsRequest() {} + + @Override + protected Call generateCall(Client client) throws StreamException { + return client.create(ThreadService.class).queryThreads(this.internalBuild()); + } + } + } + + /** + * Queries threads based on the provided parameters + * + * @return the created request + */ + @NotNull + public static QueryThreadsRequestData.QueryThreadsRequest queryThreads() { + return new QueryThreadsRequestData.QueryThreadsRequest(); + } +} diff --git a/src/main/java/io/getstream/chat/java/services/ThreadService.java b/src/main/java/io/getstream/chat/java/services/ThreadService.java new file mode 100644 index 000000000..7e1aa72d6 --- /dev/null +++ b/src/main/java/io/getstream/chat/java/services/ThreadService.java @@ -0,0 +1,13 @@ +package io.getstream.chat.java.services; + +import io.getstream.chat.java.models.Thread.QueryThreadsRequestData; +import io.getstream.chat.java.models.Thread.QueryThreadsResponse; +import org.jetbrains.annotations.NotNull; +import retrofit2.Call; +import retrofit2.http.*; + +public interface ThreadService { + @POST("threads") + Call queryThreads( + @NotNull @Body QueryThreadsRequestData queryThreadsRequestData); +} diff --git a/src/test/java/io/getstream/chat/java/BasicTest.java b/src/test/java/io/getstream/chat/java/BasicTest.java index f67d349f4..205327b45 100644 --- a/src/test/java/io/getstream/chat/java/BasicTest.java +++ b/src/test/java/io/getstream/chat/java/BasicTest.java @@ -72,7 +72,7 @@ private static void cleanChannels() throws StreamException { } // wait for the channels to delete - Assertions.assertDoesNotThrow(() -> Thread.sleep(500)); + Assertions.assertDoesNotThrow(() -> java.lang.Thread.sleep(500)); } } } @@ -109,7 +109,7 @@ private static void cleanUsers() throws StreamException { } // wait for the channels to delete - Assertions.assertDoesNotThrow(() -> Thread.sleep(500)); + Assertions.assertDoesNotThrow(() -> java.lang.Thread.sleep(500)); } } } @@ -239,7 +239,7 @@ protected static Message sendTestMessage() throws StreamException { */ protected void pause() { try { - Thread.sleep(6000); + java.lang.Thread.sleep(6000); } catch (InterruptedException e) { // Do nothing } @@ -261,7 +261,7 @@ protected static void waitFor(Supplier predicate, Long askInterval, Lon return; } - Assertions.assertDoesNotThrow(() -> Thread.sleep(askInterval)); + Assertions.assertDoesNotThrow(() -> java.lang.Thread.sleep(askInterval)); } } } diff --git a/src/test/java/io/getstream/chat/java/ExportUsersTest.java b/src/test/java/io/getstream/chat/java/ExportUsersTest.java index 9c840f0b9..a158ab11b 100644 --- a/src/test/java/io/getstream/chat/java/ExportUsersTest.java +++ b/src/test/java/io/getstream/chat/java/ExportUsersTest.java @@ -30,7 +30,7 @@ void exportUsersTest() { taskCompleted = true; break; } - Assertions.assertDoesNotThrow(() -> Thread.sleep(500)); + Assertions.assertDoesNotThrow(() -> java.lang.Thread.sleep(500)); } Assertions.assertTrue(taskCompleted); } diff --git a/src/test/java/io/getstream/chat/java/ThreadTest.java b/src/test/java/io/getstream/chat/java/ThreadTest.java new file mode 100644 index 000000000..efcaf8561 --- /dev/null +++ b/src/test/java/io/getstream/chat/java/ThreadTest.java @@ -0,0 +1,281 @@ +package io.getstream.chat.java; + +import io.getstream.chat.java.exceptions.StreamException; +import io.getstream.chat.java.models.Channel; +import io.getstream.chat.java.models.Channel.ChannelMemberRequestObject; +import io.getstream.chat.java.models.Channel.ChannelRequestObject; +import io.getstream.chat.java.models.FilterCondition; +import io.getstream.chat.java.models.Message; +import io.getstream.chat.java.models.Sort; +import io.getstream.chat.java.models.Thread; +import java.util.List; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Tests for the thread functionality. */ +public class ThreadTest extends BasicTest { + + @DisplayName("Can query threads with filter parameters") + @Test + void whenQueryingThreadsWithFilter_thenNoException() throws StreamException { + // Create a channel with a thread + String channelId = UUID.randomUUID().toString(); + Channel.ChannelGetResponse channel = + Channel.getOrCreate("messaging", channelId) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + List.of( + ChannelMemberRequestObject.builder() + .user(testUserRequestObject) + .build())) + .build()) + .request(); + + // Create a parent message for the thread + Message.MessageRequestObject parentMessage = + Message.MessageRequestObject.builder() + .text("Parent message for thread") + .userId(testUserRequestObject.getId()) + .build(); + + Message.MessageSendResponse parentMessageResponse = + Message.send(channel.getChannel().getType(), channel.getChannel().getId()) + .message(parentMessage) + .request(); + + // Create a reply to the parent message to form a thread + Message.MessageRequestObject replyMessage = + Message.MessageRequestObject.builder() + .text("Reply in thread") + .userId(testUserRequestObject.getId()) + .parentId(parentMessageResponse.getMessage().getId()) + .build(); + + Message.send(channel.getChannel().getType(), channel.getChannel().getId()) + .message(replyMessage) + .request(); + + // Query threads with filter for the specific channel + Thread.QueryThreadsResponse response = + Thread.queryThreads() + .userId(testUserRequestObject.getId()) + .filter(FilterCondition.eq("channel_cid", channel.getChannel().getCId())) + .request(); + + // Verify the response + Assertions.assertNotNull(response); + Assertions.assertNotNull(response.getThreads()); + Assertions.assertEquals(1, response.getThreads().size()); + Assertions.assertEquals( + channel.getChannel().getCId(), response.getThreads().get(0).getChannelCid()); + Assertions.assertEquals( + parentMessageResponse.getMessage().getId(), + response.getThreads().get(0).getParentMessageId()); + } + + @DisplayName("Can query threads with sort parameters") + @Test + void whenQueryingThreadsWithSort_thenNoException() throws StreamException { + // Create two channels with threads + String channel1Id = UUID.randomUUID().toString(); + Channel.ChannelGetResponse channel1 = + Channel.getOrCreate("messaging", channel1Id) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + List.of( + ChannelMemberRequestObject.builder() + .user(testUserRequestObject) + .build())) + .build()) + .request(); + + // Create a parent message for the first thread + Message.MessageRequestObject parentMessage1 = + Message.MessageRequestObject.builder() + .text("Parent message for thread 1") + .userId(testUserRequestObject.getId()) + .build(); + + Message.MessageSendResponse parentMessageResponse1 = + Message.send(channel1.getChannel().getType(), channel1.getChannel().getId()) + .message(parentMessage1) + .request(); + + // Create a reply to the parent message to form a thread + Message.MessageRequestObject replyMessage1 = + Message.MessageRequestObject.builder() + .text("Reply in thread 1") + .userId(testUserRequestObject.getId()) + .parentId(parentMessageResponse1.getMessage().getId()) + .build(); + + Message.send(channel1.getChannel().getType(), channel1.getChannel().getId()) + .message(replyMessage1) + .request(); + + // Create a second channel with a thread + String channel2Id = UUID.randomUUID().toString(); + Channel.ChannelGetResponse channel2 = + Channel.getOrCreate("messaging", channel2Id) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + List.of( + ChannelMemberRequestObject.builder() + .user(testUserRequestObject) + .build())) + .build()) + .request(); + + // Create a parent message for the second thread + Message.MessageRequestObject parentMessage2 = + Message.MessageRequestObject.builder() + .text("Parent message for thread 2") + .userId(testUserRequestObject.getId()) + .build(); + + Message.MessageSendResponse parentMessageResponse2 = + Message.send(channel2.getChannel().getType(), channel2.getChannel().getId()) + .message(parentMessage2) + .request(); + + // Create a reply to the parent message to form a thread + Message.MessageRequestObject replyMessage2 = + Message.MessageRequestObject.builder() + .text("Reply in thread 2") + .userId(testUserRequestObject.getId()) + .parentId(parentMessageResponse2.getMessage().getId()) + .build(); + + Message.send(channel2.getChannel().getType(), channel2.getChannel().getId()) + .message(replyMessage2) + .request(); + + // Query threads with sort by created_at in descending order + Thread.QueryThreadsResponse response = + Thread.queryThreads() + .userId(testUserRequestObject.getId()) + .sort(Sort.builder().field("created_at").direction(Sort.Direction.DESC).build()) + .request(); + + // Verify the response + Assertions.assertNotNull(response); + Assertions.assertNotNull(response.getThreads()); + Assertions.assertEquals(2, response.getThreads().size()); + + // The second thread should be first in the list (most recent) + Assertions.assertEquals( + channel2.getChannel().getCId(), response.getThreads().get(0).getChannelCid()); + Assertions.assertEquals( + parentMessageResponse2.getMessage().getId(), + response.getThreads().get(0).getParentMessageId()); + + // The first thread should be second in the list + Assertions.assertEquals( + channel1.getChannel().getCId(), response.getThreads().get(1).getChannelCid()); + Assertions.assertEquals( + parentMessageResponse1.getMessage().getId(), + response.getThreads().get(1).getParentMessageId()); + } + + @DisplayName("Can query threads with both filter and sort parameters") + @Test + void whenQueryingThreadsWithFilterAndSort_thenNoException() throws StreamException { + // Create a channel with multiple threads + String channelId = UUID.randomUUID().toString(); + Channel.ChannelGetResponse channel = + Channel.getOrCreate("messaging", channelId) + .data( + ChannelRequestObject.builder() + .createdBy(testUserRequestObject) + .members( + List.of( + ChannelMemberRequestObject.builder() + .user(testUserRequestObject) + .build())) + .build()) + .request(); + + // Create a parent message for the first thread + Message.MessageRequestObject parentMessage1 = + Message.MessageRequestObject.builder() + .text("Parent message for thread 1") + .userId(testUserRequestObject.getId()) + .build(); + + Message.MessageSendResponse parentMessageResponse1 = + Message.send(channel.getChannel().getType(), channel.getChannel().getId()) + .message(parentMessage1) + .request(); + + // Create a reply to the parent message to form a thread + Message.MessageRequestObject replyMessage1 = + Message.MessageRequestObject.builder() + .text("Reply in thread 1") + .userId(testUserRequestObject.getId()) + .parentId(parentMessageResponse1.getMessage().getId()) + .build(); + + Message.send(channel.getChannel().getType(), channel.getChannel().getId()) + .message(replyMessage1) + .request(); + + // Create a parent message for the second thread + Message.MessageRequestObject parentMessage2 = + Message.MessageRequestObject.builder() + .text("Parent message for thread 2") + .userId(testUserRequestObject.getId()) + .build(); + + Message.MessageSendResponse parentMessageResponse2 = + Message.send(channel.getChannel().getType(), channel.getChannel().getId()) + .message(parentMessage2) + .request(); + + // Create a reply to the parent message to form a thread + Message.MessageRequestObject replyMessage2 = + Message.MessageRequestObject.builder() + .text("Reply in thread 2") + .userId(testUserRequestObject.getId()) + .parentId(parentMessageResponse2.getMessage().getId()) + .build(); + + Message.send(channel.getChannel().getType(), channel.getChannel().getId()) + .message(replyMessage2) + .request(); + + // Query threads with filter for the specific channel and sort by created_at in descending order + Thread.QueryThreadsResponse response = + Thread.queryThreads() + .userId(testUserRequestObject.getId()) + .filter(FilterCondition.eq("channel_cid", channel.getChannel().getCId())) + .sort(Sort.builder().field("created_at").direction(Sort.Direction.DESC).build()) + .request(); + + // Verify the response + Assertions.assertNotNull(response); + Assertions.assertNotNull(response.getThreads()); + Assertions.assertEquals(2, response.getThreads().size()); + + // The second thread should be first in the list (most recent) + Assertions.assertEquals( + channel.getChannel().getCId(), response.getThreads().get(0).getChannelCid()); + Assertions.assertEquals( + parentMessageResponse2.getMessage().getId(), + response.getThreads().get(0).getParentMessageId()); + + // The first thread should be second in the list + Assertions.assertEquals( + channel.getChannel().getCId(), response.getThreads().get(1).getChannelCid()); + Assertions.assertEquals( + parentMessageResponse1.getMessage().getId(), + response.getThreads().get(1).getParentMessageId()); + } +}