From 4fde0db1d828e8bdc619f5abda70d1663453f97c Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Wed, 15 Oct 2025 14:33:05 +0200 Subject: [PATCH 1/3] Fix poll not updating when it is a thread root. --- .../chat/android/client/test/Mother.kt | 134 +++++++++++ .../api/stream-chat-android-client.api | 79 +++--- .../client/api2/mapping/EventMapping.kt | 7 + .../client/api2/model/dto/EventDtos.kt | 13 +- .../chat/android/client/events/ChatEvent.kt | 8 + .../api2/mapping/EventMappingTestArguments.kt | 15 ++ .../internal/PollExtensionsTests.kt | 8 + .../internal/EventHandlerSequential.kt | 9 + .../channel/thread/internal/ThreadLogic.kt | 30 +++ .../thread/internal/ThreadMutableState.kt | 23 ++ .../thread/internal/ThreadLogicTest.kt | 226 ++++++++++++++++++ 11 files changed, 517 insertions(+), 35 deletions(-) diff --git a/stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/Mother.kt b/stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/Mother.kt index fadc98d7ea7..af212bc8864 100644 --- a/stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/Mother.kt +++ b/stream-chat-android-client-test/src/main/java/io/getstream/chat/android/client/test/Mother.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.client.test +import io.getstream.chat.android.client.events.AnswerCastedEvent import io.getstream.chat.android.client.events.ChannelDeletedEvent import io.getstream.chat.android.client.events.ChannelHiddenEvent import io.getstream.chat.android.client.events.ChannelUpdatedByUserEvent @@ -39,7 +40,9 @@ import io.getstream.chat.android.client.events.NotificationMessageNewEvent import io.getstream.chat.android.client.events.NotificationMutesUpdatedEvent import io.getstream.chat.android.client.events.NotificationReminderDueEvent import io.getstream.chat.android.client.events.NotificationRemovedFromChannelEvent +import io.getstream.chat.android.client.events.PollClosedEvent import io.getstream.chat.android.client.events.PollDeletedEvent +import io.getstream.chat.android.client.events.PollUpdatedEvent import io.getstream.chat.android.client.events.ReactionNewEvent import io.getstream.chat.android.client.events.ReminderCreatedEvent import io.getstream.chat.android.client.events.ReminderDeletedEvent @@ -48,9 +51,13 @@ import io.getstream.chat.android.client.events.TypingStartEvent import io.getstream.chat.android.client.events.TypingStopEvent import io.getstream.chat.android.client.events.UserMessagesDeletedEvent import io.getstream.chat.android.client.events.UserStartWatchingEvent +import io.getstream.chat.android.client.events.VoteCastedEvent +import io.getstream.chat.android.client.events.VoteChangedEvent +import io.getstream.chat.android.client.events.VoteRemovedEvent import io.getstream.chat.android.client.extensions.cidToTypeAndId import io.getstream.chat.android.client.parser2.adapters.internal.StreamDateFormatter import io.getstream.chat.android.client.query.QueryChannelsSpec +import io.getstream.chat.android.models.Answer import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.EventType import io.getstream.chat.android.models.FilterObject @@ -61,6 +68,7 @@ import io.getstream.chat.android.models.NeutralFilterObject import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.Reaction import io.getstream.chat.android.models.User +import io.getstream.chat.android.models.Vote import io.getstream.chat.android.models.querysort.QuerySortByField import io.getstream.chat.android.models.querysort.QuerySorter import io.getstream.chat.android.positiveRandomInt @@ -73,6 +81,8 @@ import io.getstream.chat.android.randomMember import io.getstream.chat.android.randomMessage import io.getstream.chat.android.randomMessageReminder import io.getstream.chat.android.randomPoll +import io.getstream.chat.android.randomPollAnswer +import io.getstream.chat.android.randomPollVote import io.getstream.chat.android.randomReaction import io.getstream.chat.android.randomString import io.getstream.chat.android.randomUser @@ -707,6 +717,7 @@ public fun randomUserMessagesDeletedEvent( public fun randomPollDeletedEvent( createdAt: Date = randomDate(), cid: String = randomCID(), + messageId: String = randomString(), poll: Poll = randomPoll(), ): PollDeletedEvent { val (type, id) = cid.cidToTypeAndId() @@ -717,6 +728,129 @@ public fun randomPollDeletedEvent( cid = cid, channelType = type, channelId = id, + messageId = messageId, + poll = poll, + ) +} + +public fun randomPollUpdatedEvent( + createdAt: Date = randomDate(), + cid: String = randomCID(), + messageId: String = randomString(), + poll: Poll = randomPoll(), +): PollUpdatedEvent { + val (type, id) = cid.cidToTypeAndId() + return PollUpdatedEvent( + type = EventType.POLL_UPDATED, + createdAt = createdAt, + rawCreatedAt = streamFormatter.format(createdAt), + cid = cid, + channelType = type, + channelId = id, + messageId = messageId, + poll = poll, + ) +} + +public fun randomPollClosedEvent( + createdAt: Date = randomDate(), + cid: String = randomCID(), + messageId: String = randomString(), + poll: Poll = randomPoll(), +): PollClosedEvent { + val (type, id) = cid.cidToTypeAndId() + return PollClosedEvent( + type = EventType.POLL_CLOSED, + createdAt = createdAt, + rawCreatedAt = streamFormatter.format(createdAt), + cid = cid, + channelType = type, + channelId = id, + messageId = messageId, + poll = poll, + ) +} + +public fun randomVoteCastedEvent( + createdAt: Date = randomDate(), + cid: String = randomCID(), + messageId: String = randomString(), + poll: Poll = randomPoll(), + newVote: Vote = randomPollVote(), +): VoteCastedEvent { + val (type, id) = cid.cidToTypeAndId() + return VoteCastedEvent( + type = EventType.POLL_VOTE_CASTED, + createdAt = createdAt, + rawCreatedAt = streamFormatter.format(createdAt), + cid = cid, + channelType = type, + channelId = id, + messageId = messageId, + poll = poll, + newVote = newVote, + ) +} + +public fun randomAnswerCastedEvent( + createdAt: Date = randomDate(), + cid: String = randomCID(), + messageId: String = randomString(), + poll: Poll = randomPoll(), + newAnswer: Answer = randomPollAnswer(), +): AnswerCastedEvent { + val (type, id) = cid.cidToTypeAndId() + return AnswerCastedEvent( + type = EventType.POLL_VOTE_CASTED, + createdAt = createdAt, + rawCreatedAt = streamFormatter.format(createdAt), + cid = cid, + channelType = type, + channelId = id, + messageId = messageId, + poll = poll, + newAnswer = newAnswer, + ) +} + +public fun randomVoteChangedEvent( + createdAt: Date = randomDate(), + cid: String = randomCID(), + messageId: String = randomString(), + poll: Poll = randomPoll(), + newVote: Vote = randomPollVote(), +): VoteChangedEvent { + val (type, id) = cid.cidToTypeAndId() + return VoteChangedEvent( + type = EventType.POLL_VOTE_CHANGED, + createdAt = createdAt, + rawCreatedAt = streamFormatter.format(createdAt), + cid = cid, + channelType = type, + channelId = id, + messageId = messageId, + poll = poll, + newVote = newVote, + ) +} + +public fun randomVoteRemovedEvent( + createdAt: Date = randomDate(), + cid: String = randomCID(), + messageId: String = randomString(), + poll: Poll = randomPoll(), + removedVote: Vote = randomPollVote(), +): VoteRemovedEvent { + val (type, id) = cid.cidToTypeAndId() + return VoteRemovedEvent( + type = EventType.POLL_VOTE_REMOVED, + createdAt = createdAt, + rawCreatedAt = streamFormatter.format(createdAt), + cid = cid, + channelType = type, + channelId = id, + messageId = messageId, poll = poll, + removedVote = removedVote, ) } diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index dcf5ec7e6b2..1dcae964c96 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -1027,22 +1027,24 @@ public final class io/getstream/chat/android/client/events/AIIndicatorUpdatedEve } public final class io/getstream/chat/android/client/events/AnswerCastedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun component8 ()Lio/getstream/chat/android/models/Answer; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;)Lio/getstream/chat/android/client/events/AnswerCastedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/AnswerCastedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/AnswerCastedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun component9 ()Lio/getstream/chat/android/models/Answer; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;)Lio/getstream/chat/android/client/events/AnswerCastedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/AnswerCastedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/AnswerCastedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public final fun getNewAnswer ()Lio/getstream/chat/android/models/Answer; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; @@ -1443,6 +1445,7 @@ public abstract interface class io/getstream/chat/android/client/events/HasOwnUs } public abstract interface class io/getstream/chat/android/client/events/HasPoll { + public abstract fun getMessageId ()Ljava/lang/String; public abstract fun getPoll ()Lio/getstream/chat/android/models/Poll; } @@ -2091,21 +2094,23 @@ public final class io/getstream/chat/android/client/events/NotificationThreadMes } public final class io/getstream/chat/android/client/events/PollClosedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/client/events/PollClosedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/PollClosedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/PollClosedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/client/events/PollClosedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/PollClosedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/PollClosedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; public fun getType ()Ljava/lang/String; @@ -2114,21 +2119,23 @@ public final class io/getstream/chat/android/client/events/PollClosedEvent : io/ } public final class io/getstream/chat/android/client/events/PollDeletedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/client/events/PollDeletedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/PollDeletedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/PollDeletedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/client/events/PollDeletedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/PollDeletedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/PollDeletedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; public fun getType ()Ljava/lang/String; @@ -2137,21 +2144,23 @@ public final class io/getstream/chat/android/client/events/PollDeletedEvent : io } public final class io/getstream/chat/android/client/events/PollUpdatedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/client/events/PollUpdatedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/PollUpdatedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/PollUpdatedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;)Lio/getstream/chat/android/client/events/PollUpdatedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/PollUpdatedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/PollUpdatedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; public fun getType ()Ljava/lang/String; @@ -2523,22 +2532,24 @@ public final class io/getstream/chat/android/client/events/UserUpdatedEvent : io } public final class io/getstream/chat/android/client/events/VoteCastedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun component8 ()Lio/getstream/chat/android/models/Vote; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)Lio/getstream/chat/android/client/events/VoteCastedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/VoteCastedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/VoteCastedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun component9 ()Lio/getstream/chat/android/models/Vote; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)Lio/getstream/chat/android/client/events/VoteCastedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/VoteCastedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/VoteCastedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public final fun getNewVote ()Lio/getstream/chat/android/models/Vote; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; @@ -2548,22 +2559,24 @@ public final class io/getstream/chat/android/client/events/VoteCastedEvent : io/ } public final class io/getstream/chat/android/client/events/VoteChangedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun component8 ()Lio/getstream/chat/android/models/Vote; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)Lio/getstream/chat/android/client/events/VoteChangedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/VoteChangedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/VoteChangedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun component9 ()Lio/getstream/chat/android/models/Vote; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)Lio/getstream/chat/android/client/events/VoteChangedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/VoteChangedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/VoteChangedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public final fun getNewVote ()Lio/getstream/chat/android/models/Vote; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; @@ -2573,22 +2586,24 @@ public final class io/getstream/chat/android/client/events/VoteChangedEvent : io } public final class io/getstream/chat/android/client/events/VoteRemovedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { - public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/Date; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; public final fun component6 ()Ljava/lang/String; - public final fun component7 ()Lio/getstream/chat/android/models/Poll; - public final fun component8 ()Lio/getstream/chat/android/models/Vote; - public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)Lio/getstream/chat/android/client/events/VoteRemovedEvent; - public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/VoteRemovedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/VoteRemovedEvent; + public final fun component7 ()Ljava/lang/String; + public final fun component8 ()Lio/getstream/chat/android/models/Poll; + public final fun component9 ()Lio/getstream/chat/android/models/Vote; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;)Lio/getstream/chat/android/client/events/VoteRemovedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/VoteRemovedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Vote;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/VoteRemovedEvent; public fun equals (Ljava/lang/Object;)Z public fun getChannelId ()Ljava/lang/String; public fun getChannelType ()Ljava/lang/String; public fun getCid ()Ljava/lang/String; public fun getCreatedAt ()Ljava/util/Date; + public fun getMessageId ()Ljava/lang/String; public fun getPoll ()Lio/getstream/chat/android/models/Poll; public fun getRawCreatedAt ()Ljava/lang/String; public final fun getRemovedVote ()Lio/getstream/chat/android/models/Vote; diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt index 145096048cd..82ee31e116b 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt @@ -930,6 +930,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = newPoll, ) } @@ -947,6 +948,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = newPoll, ) } @@ -964,6 +966,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = newPoll, ) } @@ -991,6 +994,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = newPoll, newVote = pollVote, ) @@ -1009,6 +1013,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = poll.toDomain(), newAnswer = newAnswer, ) @@ -1037,6 +1042,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = newPoll, newVote = pollVote, ) @@ -1056,6 +1062,7 @@ internal class EventMapping( cid = cid, channelType = channelType, channelId = channelId, + messageId = message_id, poll = newPoll, removedVote = removedVote, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt index 4ee61dfad76..1a85cc3045e 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt @@ -508,14 +508,16 @@ internal data class UserUpdatedEventDto( internal data class PollUpdatedEventDto( val type: String, val cid: String, - val poll: DownstreamPollDto, + val message_id: String?, val created_at: ExactDate, + val poll: DownstreamPollDto, ) : ChatEventDto() @JsonClass(generateAdapter = true) internal data class PollDeletedEventDto( val type: String, val cid: String, + val message_id: String?, val created_at: ExactDate, val poll: DownstreamPollDto, ) : ChatEventDto() @@ -524,6 +526,7 @@ internal data class PollDeletedEventDto( internal data class PollClosedEventDto( val type: String, val cid: String, + val message_id: String?, val created_at: ExactDate, val poll: DownstreamPollDto, ) : ChatEventDto() @@ -532,6 +535,7 @@ internal data class PollClosedEventDto( internal data class VoteCastedEventDto( val type: String, val cid: String, + val message_id: String?, val created_at: ExactDate, val poll: DownstreamPollDto, val poll_vote: DownstreamVoteDto, @@ -541,6 +545,7 @@ internal data class VoteCastedEventDto( internal data class AnswerCastedEventDto( val type: String, val cid: String, + val message_id: String?, val created_at: ExactDate, val poll: DownstreamPollDto, val poll_vote: DownstreamVoteDto, @@ -550,8 +555,9 @@ internal data class AnswerCastedEventDto( internal data class VoteChangedEventDto( val type: String, val cid: String, - val poll: DownstreamPollDto, + val message_id: String?, val created_at: ExactDate, + val poll: DownstreamPollDto, val poll_vote: DownstreamVoteDto, ) : ChatEventDto() @@ -559,8 +565,9 @@ internal data class VoteChangedEventDto( internal data class VoteRemovedEventDto( val type: String, val cid: String, - val poll: DownstreamPollDto, + val message_id: String?, val created_at: ExactDate, + val poll: DownstreamPollDto, val poll_vote: DownstreamVoteDto, ) : ChatEventDto() diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt index 66dddcdb1c5..ab9ef63b715 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt @@ -74,6 +74,7 @@ public sealed interface HasOwnUser { } public sealed interface HasPoll { + public val messageId: String? public val poll: Poll } @@ -736,6 +737,7 @@ public data class PollUpdatedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, ) : CidEvent(), HasPoll @@ -749,6 +751,7 @@ public data class PollDeletedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, ) : CidEvent(), HasPoll @@ -762,6 +765,7 @@ public data class PollClosedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, ) : CidEvent(), HasPoll @@ -775,6 +779,7 @@ public data class VoteCastedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, val newVote: Vote, ) : CidEvent(), HasPoll @@ -789,6 +794,7 @@ public data class AnswerCastedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, val newAnswer: Answer, ) : CidEvent(), HasPoll @@ -803,6 +809,7 @@ public data class VoteChangedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, val newVote: Vote, ) : CidEvent(), HasPoll @@ -817,6 +824,7 @@ public data class VoteRemovedEvent( override val cid: String, override val channelType: String, override val channelId: String, + override val messageId: String?, override val poll: Poll, val removedVote: Vote, ) : CidEvent(), HasPoll diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt index 5cdd30337fc..d31ebca8402 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/api2/mapping/EventMappingTestArguments.kt @@ -184,6 +184,7 @@ internal object EventMappingTestArguments { private val CHANNEL_MEMBER_COUNT = positiveRandomInt() private val CHANNEL_NAME = randomString() private val CHANNEL_IMAGE = randomString() + private val MESSAGE_ID = randomString() private val MESSAGE = Mother.randomDownstreamMessageDto() private val MESSAGE_WITHOUT_CHANNEL_INFO = MESSAGE.copy(channel = null) private val DRAFT = Mother.randomDownstreamDraftDto() @@ -669,6 +670,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_CLOSED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, ) @@ -676,6 +678,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_DELETED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, ) @@ -683,6 +686,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_UPDATED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, ) @@ -690,6 +694,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_VOTE_CASTED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, poll_vote = POLL_VOTE, ) @@ -698,6 +703,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_VOTE_CHANGED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, poll_vote = POLL_VOTE, ) @@ -706,6 +712,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_VOTE_REMOVED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, poll_vote = POLL_VOTE, ) @@ -714,6 +721,7 @@ internal object EventMappingTestArguments { type = EventType.POLL_VOTE_CASTED, created_at = EXACT_DATE, cid = CID, + message_id = MESSAGE_ID, poll = POLL, poll_vote = POLL_VOTE, ) @@ -1333,6 +1341,7 @@ internal object EventMappingTestArguments { cid = pollClosedDto.cid, channelType = pollClosedDto.cid.split(":").first(), channelId = pollClosedDto.cid.split(":").last(), + messageId = pollClosedDto.message_id, poll = with(domainMapping) { pollClosedDto.poll.toDomain() }, ) @@ -1343,6 +1352,7 @@ internal object EventMappingTestArguments { cid = pollDeletedDto.cid, channelType = pollDeletedDto.cid.split(":").first(), channelId = pollDeletedDto.cid.split(":").last(), + messageId = pollDeletedDto.message_id, poll = with(domainMapping) { pollDeletedDto.poll.toDomain() }, ) @@ -1353,6 +1363,7 @@ internal object EventMappingTestArguments { cid = pollUpdatedDto.cid, channelType = pollUpdatedDto.cid.split(":").first(), channelId = pollUpdatedDto.cid.split(":").last(), + messageId = pollUpdatedDto.message_id, poll = with(domainMapping) { pollUpdatedDto.poll.toDomain() }, ) @@ -1363,6 +1374,7 @@ internal object EventMappingTestArguments { cid = voteCastedDto.cid, channelType = voteCastedDto.cid.split(":").first(), channelId = voteCastedDto.cid.split(":").last(), + messageId = voteCastedDto.message_id, poll = with(domainMapping) { voteCastedDto.poll.toDomain() }, newVote = with(domainMapping) { voteCastedDto.poll_vote.toDomain() }, ) @@ -1374,6 +1386,7 @@ internal object EventMappingTestArguments { cid = voteChangedDto.cid, channelType = voteChangedDto.cid.split(":").first(), channelId = voteChangedDto.cid.split(":").last(), + messageId = voteChangedDto.message_id, poll = with(domainMapping) { voteChangedDto.poll.toDomain() }, newVote = with(domainMapping) { voteChangedDto.poll_vote.toDomain() }, ) @@ -1385,6 +1398,7 @@ internal object EventMappingTestArguments { cid = voteRemovedDto.cid, channelType = voteRemovedDto.cid.split(":").first(), channelId = voteRemovedDto.cid.split(":").last(), + messageId = voteRemovedDto.message_id, poll = with(domainMapping) { voteRemovedDto.poll.toDomain() }, removedVote = with(domainMapping) { voteRemovedDto.poll_vote.toDomain() }, ) @@ -1396,6 +1410,7 @@ internal object EventMappingTestArguments { cid = answerCastedDto.cid, channelType = answerCastedDto.cid.split(":").first(), channelId = answerCastedDto.cid.split(":").last(), + messageId = answerCastedDto.message_id, poll = with(domainMapping) { answerCastedDto.poll.toDomain() }, newAnswer = with(domainMapping) { answerCastedDto.poll_vote.toAnswerDomain() }, ) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/PollExtensionsTests.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/PollExtensionsTests.kt index ad4488cad5e..8a9c2ac68e4 100644 --- a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/PollExtensionsTests.kt +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/PollExtensionsTests.kt @@ -100,6 +100,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, newVote = newVote, ) @@ -123,6 +124,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, newVote = newVote, ) @@ -153,6 +155,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, newVote = newVote, ) @@ -176,6 +179,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, removedVote = vote1, ) @@ -206,6 +210,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, newAnswer = newAnswer, ) @@ -229,6 +234,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, ) @@ -249,6 +255,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, ) @@ -270,6 +277,7 @@ internal class PollExtensionsTests { cid = "channel1", channelType = "messaging", channelId = "channel1", + messageId = "message1", poll = basePoll, newVote = vote1, ) diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt index 1a55c0483ae..3402c41e78f 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt @@ -492,6 +492,15 @@ internal class EventHandlerSequential( .forEach { (messageId, events) -> logicRegistry.thread(messageId).handleReminderEvents(events) } + sortedEvents.filterIsInstance() + .groupBy { it.messageId } + .filterKeys { it != null && logicRegistry.isActiveThread(it) } + .forEach { (messageId, events) -> + messageId?.let { + logicRegistry.thread(it).handlePollEvents(currentUserId, events) + } + } + logger.v { "[updateThreadState] completed batchId: ${batchEvent.id}" } } diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt index 14a3689817e..4c1daddd40d 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt @@ -16,12 +16,21 @@ package io.getstream.chat.android.state.plugin.logic.channel.thread.internal +import io.getstream.chat.android.client.events.AnswerCastedEvent import io.getstream.chat.android.client.events.HasMessage +import io.getstream.chat.android.client.events.HasPoll import io.getstream.chat.android.client.events.HasReminder import io.getstream.chat.android.client.events.MessageUpdatedEvent +import io.getstream.chat.android.client.events.PollClosedEvent +import io.getstream.chat.android.client.events.PollDeletedEvent +import io.getstream.chat.android.client.events.PollUpdatedEvent import io.getstream.chat.android.client.events.ReminderCreatedEvent import io.getstream.chat.android.client.events.ReminderDeletedEvent import io.getstream.chat.android.client.events.ReminderUpdatedEvent +import io.getstream.chat.android.client.events.VoteCastedEvent +import io.getstream.chat.android.client.events.VoteChangedEvent +import io.getstream.chat.android.client.events.VoteRemovedEvent +import io.getstream.chat.android.client.extensions.internal.processPoll import io.getstream.chat.android.client.extensions.internal.toMessageReminderInfo import io.getstream.chat.android.client.plugin.listeners.ThreadQueryListener import io.getstream.chat.android.models.Message @@ -128,4 +137,25 @@ internal class ThreadLogic( upsertMessages(messages) } } + + internal fun handlePollEvents(currentUserId: String?, events: List) { + // Don't handle poll events if there is no poll in the parent message (should never happen) + val parentMessage = mutableState.parentMessage ?: return + val poll = parentMessage.poll ?: return + // Don't handle poll events if the poll in the parent message is different (should never happen) + events + .filter { it.poll.id == poll.id } + .forEach { event -> + val processedPoll = when (event) { + is AnswerCastedEvent -> event.processPoll { poll } + is PollClosedEvent -> event.processPoll { poll } + is PollUpdatedEvent -> event.processPoll { poll } + is VoteRemovedEvent -> event.processPoll { poll } + is VoteCastedEvent -> event.processPoll(currentUserId) { poll } + is VoteChangedEvent -> event.processPoll(currentUserId) { poll } + is PollDeletedEvent -> null // poll is deleted, remove from state + } + mutableState.updateParentMessagePoll(processedPoll) + } + } } diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/channel/thread/internal/ThreadMutableState.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/channel/thread/internal/ThreadMutableState.kt index 9e1fe5819fa..9b8965fe21b 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/channel/thread/internal/ThreadMutableState.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/state/channel/thread/internal/ThreadMutableState.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.state.plugin.state.channel.thread.internal import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.state.plugin.state.channel.thread.ThreadState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow @@ -56,6 +57,12 @@ internal class ThreadMutableState( override val oldestInThread: StateFlow = _oldestInThread!! override val newestInThread: StateFlow = _newestInThread!! + /** + * Retrieves the parent message of the thread. + */ + val parentMessage: Message? + get() = _messages?.value?.get(parentId) + fun setLoading(isLoading: Boolean) { _loading?.value = isLoading } @@ -84,6 +91,22 @@ internal class ThreadMutableState( _messages?.apply { value += (messages.associateBy(Message::id) - deletedMessagesIds) } } + /** + * Updates the poll object related to the parent message of the thread. + * Note: This is relevant only for the parent message, as polls cannot be added to replies. + * + * @param poll The updated poll object. + */ + fun updateParentMessagePoll(poll: Poll?) { + val parent = parentMessage ?: return + val parentPoll = parent.poll ?: return + // Allow deleting poll (when poll == null), or overriding the poll (when poll.id == parent.poll.id) + if (poll == null || poll.id == parentPoll.id) { + val updatedParent = parent.copy(poll = poll) + upsertMessages(listOf(updatedParent)) + } + } + fun destroy() { _messages = null _loading = null diff --git a/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt b/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt index 8d72f85aa86..001c6071e22 100644 --- a/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt +++ b/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt @@ -18,14 +18,22 @@ package io.getstream.chat.android.state.plugin.logic.channel.thread.internal import io.getstream.chat.android.client.events.HasReminder import io.getstream.chat.android.client.extensions.internal.toMessageReminderInfo +import io.getstream.chat.android.client.test.randomAnswerCastedEvent import io.getstream.chat.android.client.test.randomNotificationReminderDueEvent +import io.getstream.chat.android.client.test.randomPollClosedEvent +import io.getstream.chat.android.client.test.randomPollDeletedEvent +import io.getstream.chat.android.client.test.randomPollUpdatedEvent import io.getstream.chat.android.client.test.randomReminderCreatedEvent import io.getstream.chat.android.client.test.randomReminderDeletedEvent import io.getstream.chat.android.client.test.randomReminderUpdatedEvent +import io.getstream.chat.android.client.test.randomVoteCastedEvent +import io.getstream.chat.android.client.test.randomVoteChangedEvent +import io.getstream.chat.android.client.test.randomVoteRemovedEvent import io.getstream.chat.android.models.MessageReminderInfo import io.getstream.chat.android.randomDate import io.getstream.chat.android.randomMessage import io.getstream.chat.android.randomMessageReminder +import io.getstream.chat.android.randomPoll import io.getstream.chat.android.randomString import io.getstream.chat.android.state.plugin.state.channel.thread.internal.ThreadMutableState import kotlinx.coroutines.flow.MutableStateFlow @@ -273,4 +281,222 @@ internal class ThreadLogicTest { val expectedMessage = existingMessage.copy(reminder = newReminder.toMessageReminderInfo()) verify(threadStateLogic, times(1)).upsertMessages(listOf(expectedMessage)) } + + @Test + fun `Given no parent message When handlePollEvents is called Should not update poll`() { + // given + val poll = randomPoll() + val event = randomPollUpdatedEvent(poll = poll) + + whenever(threadMutableState.parentMessage).doReturn(null) + + // when + threadLogic.handlePollEvents(currentUserId = randomString(), events = listOf(event)) + + // then + verify(threadMutableState, never()).updateParentMessagePoll(any()) + } + + @Test + fun `Given parent message without poll When handlePollEvents is called Should not update poll`() { + // given + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = null) + val poll = randomPoll() + val event = randomPollUpdatedEvent(poll = poll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = randomString(), events = listOf(event)) + + // then + verify(threadMutableState, never()).updateParentMessagePoll(any()) + } + + @Test + fun `Given PollUpdatedEvent with matching poll ID When handlePollEvents is called Should update poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val updatedPoll = randomPoll(id = pollId) + val event = randomPollUpdatedEvent(poll = updatedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(any()) + } + + @Test + fun `Given PollClosedEvent with matching poll ID When handlePollEvents is called Should update poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val updatedPoll = randomPoll(id = pollId) + val event = randomPollClosedEvent(poll = updatedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(any()) + } + + @Test + fun `Given VoteCastedEvent with matching poll ID When handlePollEvents is called Should update poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val updatedPoll = randomPoll(id = pollId) + val event = randomVoteCastedEvent(poll = updatedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(any()) + } + + @Test + fun `Given VoteChangedEvent with matching poll ID When handlePollEvents is called Should update poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val updatedPoll = randomPoll(id = pollId) + val event = randomVoteChangedEvent(poll = updatedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(any()) + } + + @Test + fun `Given VoteRemovedEvent with matching poll ID When handlePollEvents is called Should update poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val updatedPoll = randomPoll(id = pollId) + val event = randomVoteRemovedEvent(poll = updatedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(any()) + } + + @Test + fun `Given AnswerCastedEvent with matching poll ID When handlePollEvents is called Should update poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val updatedPoll = randomPoll(id = pollId) + val event = randomAnswerCastedEvent(poll = updatedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(any()) + } + + @Test + fun `Given PollDeletedEvent with matching poll ID When handlePollEvents is called Should update poll to null`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val deletedPoll = randomPoll(id = pollId) + val event = randomPollDeletedEvent(poll = deletedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(null) + } + + @Test + fun `Given event with non-matching poll ID When handlePollEvents is called Should not update poll`() { + // given + val pollId = randomString() + val differentPollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val differentPoll = randomPoll(id = differentPollId) + val event = randomPollUpdatedEvent(poll = differentPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event)) + + // then + verify(threadMutableState, never()).updateParentMessagePoll(any()) + } + + @Test + fun `Given multiple poll events with mixed poll IDs When handlePollEvents is called Should only update poll for matching events`() { + // given + val pollId = randomString() + val differentPollId = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + val matchingPoll = randomPoll(id = pollId) + val nonMatchingPoll = randomPoll(id = differentPollId) + + val matchingEvent1 = randomPollUpdatedEvent(poll = matchingPoll) + val nonMatchingEvent = randomVoteCastedEvent(poll = nonMatchingPoll) + val matchingEvent2 = randomPollClosedEvent(poll = matchingPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(matchingEvent1, nonMatchingEvent, matchingEvent2)) + + // then + verify(threadMutableState, times(2)).updateParentMessagePoll(any()) + } } From 39efad43661a7a5826b1f0b9c677af47be3048ce Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Wed, 15 Oct 2025 14:47:30 +0200 Subject: [PATCH 2/3] Update CHANGELOG.md. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738617216fe..f3b3222dd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ ## stream-chat-android-state ### 🐞 Fixed +- Fix polls not updated live when they are a thread parent message. [#5968](https://github.com/GetStream/stream-chat-android/pull/5968) ### ⬆️ Improved From fcea8ad6d9eed2b68baf5fb4192c5fccb8fa3b71 Mon Sep 17 00:00:00 2001 From: VelikovPetar Date: Tue, 21 Oct 2025 12:00:57 +0200 Subject: [PATCH 3/3] Ensure poll events are processed in a batch. --- .../channel/thread/internal/ThreadLogic.kt | 21 ++-- .../thread/internal/ThreadLogicTest.kt | 113 ++++++++++++++++-- 2 files changed, 119 insertions(+), 15 deletions(-) diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt index 5ad766dcc72..c88351edaf1 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt @@ -34,6 +34,7 @@ import io.getstream.chat.android.client.extensions.internal.processPoll import io.getstream.chat.android.client.extensions.internal.toMessageReminderInfo import io.getstream.chat.android.client.plugin.listeners.ThreadQueryListener import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.Poll import io.getstream.chat.android.state.plugin.state.channel.thread.internal.ThreadMutableState /** Logic class for thread state management. Implements [ThreadQueryListener] as listener for LLC requests. */ @@ -146,20 +147,24 @@ internal class ThreadLogic( // Don't handle poll events if there is no poll in the parent message (should never happen) val parentMessage = mutableState.parentMessage ?: return val poll = parentMessage.poll ?: return + // The processed poll after applying each event sequentially + var processedPoll: Poll? = poll // Don't handle poll events if the poll in the parent message is different (should never happen) events .filter { it.poll.id == poll.id } .forEach { event -> - val processedPoll = when (event) { - is AnswerCastedEvent -> event.processPoll { poll } - is PollClosedEvent -> event.processPoll { poll } - is PollUpdatedEvent -> event.processPoll { poll } - is VoteRemovedEvent -> event.processPoll { poll } - is VoteCastedEvent -> event.processPoll(currentUserId) { poll } - is VoteChangedEvent -> event.processPoll(currentUserId) { poll } + processedPoll = when (event) { + is AnswerCastedEvent -> event.processPoll { processedPoll } + is PollClosedEvent -> event.processPoll { processedPoll } + is PollUpdatedEvent -> event.processPoll { processedPoll } + is VoteRemovedEvent -> event.processPoll { processedPoll } + is VoteCastedEvent -> event.processPoll(currentUserId) { processedPoll } + is VoteChangedEvent -> event.processPoll(currentUserId) { processedPoll } is PollDeletedEvent -> null // poll is deleted, remove from state } - mutableState.updateParentMessagePoll(processedPoll) } + if (processedPoll != poll) { + mutableState.updateParentMessagePoll(processedPoll) + } } } diff --git a/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt b/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt index 8a483dd9507..651a1b042fa 100644 --- a/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt +++ b/stream-chat-android-state/src/test/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogicTest.kt @@ -35,6 +35,7 @@ import io.getstream.chat.android.randomDate import io.getstream.chat.android.randomMessage import io.getstream.chat.android.randomMessageReminder import io.getstream.chat.android.randomPoll +import io.getstream.chat.android.randomPollOption import io.getstream.chat.android.randomReaction import io.getstream.chat.android.randomString import io.getstream.chat.android.state.plugin.state.channel.thread.internal.ThreadMutableState @@ -106,7 +107,8 @@ internal class ThreadLogicTest { fun `Given ReminderDeletedEvent When handleReminderEvents is called Should upsert message with null reminder`() { // given val messageId = randomString() - val existingMessage = randomMessage(id = messageId, reminder = MessageReminderInfo(Date(), randomDate(), randomDate())) + val existingMessage = + randomMessage(id = messageId, reminder = MessageReminderInfo(Date(), randomDate(), randomDate())) val reminder = randomMessageReminder(messageId = messageId, message = existingMessage) val event = randomReminderDeletedEvent(messageId = messageId, reminder = reminder) @@ -444,11 +446,11 @@ internal class ThreadLogicTest { // given val pollId = randomString() val currentUserId = randomString() - val poll = randomPoll(id = pollId) + val poll = randomPoll(id = pollId, closed = false) val parentMessageId = randomString() val parentMessage = randomMessage(id = parentMessageId, poll = poll) - val updatedPoll = randomPoll(id = pollId) - val event = randomPollClosedEvent(poll = updatedPoll) + val closedPoll = poll.copy(closed = true) + val event = randomPollClosedEvent(poll = closedPoll) whenever(threadMutableState.parentMessage).doReturn(parentMessage) @@ -594,14 +596,111 @@ internal class ThreadLogicTest { val matchingEvent1 = randomPollUpdatedEvent(poll = matchingPoll) val nonMatchingEvent = randomVoteCastedEvent(poll = nonMatchingPoll) - val matchingEvent2 = randomPollClosedEvent(poll = matchingPoll) whenever(threadMutableState.parentMessage).doReturn(parentMessage) // when - threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(matchingEvent1, nonMatchingEvent, matchingEvent2)) + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(matchingEvent1, nonMatchingEvent)) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(matchingEvent1.poll) + } + + @Test + fun `Given batch of poll events When handlePollEvents is called Should process all events sequentially and update poll state`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val optionId1 = randomPollOption() + val optionId2 = randomPollOption() + val initialPoll = randomPoll( + id = pollId, + options = listOf(optionId1, optionId2), + voteCountsByOption = mapOf(optionId1.id to 0, optionId2.id to 0), + closed = false, + ) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = initialPoll) + + val updatedPoll = initialPoll.copy( + voteCountsByOption = mapOf(optionId1.id to 2, optionId2.id to 0), + closed = false, + ) + val pollUpdatedEvent = randomPollUpdatedEvent(poll = updatedPoll) + val closedPoll = updatedPoll.copy(closed = true) + val pollClosedEvent = randomPollClosedEvent(poll = closedPoll) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents( + currentUserId = currentUserId, + events = listOf(pollUpdatedEvent, pollClosedEvent), + ) + + // then + val expectedPoll = initialPoll.copy( + voteCountsByOption = mapOf(optionId1.id to 2, optionId2.id to 0), + closed = true, + ) + verify(threadMutableState, times(1)).updateParentMessagePoll(expectedPoll) + } + + @Test + fun `Given batch with poll deleted at end When handlePollEvents is called Should end with null poll`() { + // given + val pollId = randomString() + val currentUserId = randomString() + val optionId = randomString() + val initialPoll = randomPoll( + id = pollId, + voteCountsByOption = mapOf(optionId to 0), + closed = false, + ) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = initialPoll) + + val voteCastedEvent = randomVoteCastedEvent( + poll = randomPoll(id = pollId, voteCountsByOption = mapOf(optionId to 1)), + ) + val pollClosedEvent = randomPollClosedEvent( + poll = randomPoll(id = pollId, voteCountsByOption = mapOf(optionId to 1), closed = true), + ) + val pollDeletedEvent = randomPollDeletedEvent(poll = randomPoll(id = pollId)) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents( + currentUserId = currentUserId, + events = listOf(voteCastedEvent, pollClosedEvent, pollDeletedEvent), + ) + + // then + verify(threadMutableState, times(1)).updateParentMessagePoll(null) + } + + @Test + fun `Given batch with all non-matching poll IDs When handlePollEvents is called Should not update poll`() { + // given + val pollId = randomString() + val differentPollId1 = randomString() + val differentPollId2 = randomString() + val currentUserId = randomString() + val poll = randomPoll(id = pollId) + val parentMessageId = randomString() + val parentMessage = randomMessage(id = parentMessageId, poll = poll) + + val event1 = randomVoteCastedEvent(poll = randomPoll(id = differentPollId1)) + val event2 = randomPollUpdatedEvent(poll = randomPoll(id = differentPollId2)) + val event3 = randomPollClosedEvent(poll = randomPoll(id = differentPollId1)) + + whenever(threadMutableState.parentMessage).doReturn(parentMessage) + + // when + threadLogic.handlePollEvents(currentUserId = currentUserId, events = listOf(event1, event2, event3)) // then - verify(threadMutableState, times(2)).updateParentMessagePoll(any()) + verify(threadMutableState, never()).updateParentMessagePoll(any()) } }