Skip to content

Commit aa31083

Browse files
authored
Fix poll data overridden by message.updated event (#5963)
* Fix poll data overridden by `message.updated` event. * Update CHANGELOG.md. * Ensure message.update doesn't override poll data in DB.
1 parent 0b7bfa7 commit aa31083

File tree

8 files changed

+337
-3
lines changed

8 files changed

+337
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
## stream-chat-android-state
3636
### 🐞 Fixed
37+
- Fix poll state getting overridden by `message.new` events. [#5963](https://github.com/GetStream/stream-chat-android/pull/5963)
3738

3839
### ⬆️ Improved
3940

stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Thread.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ public fun Thread.updateParentOrReply(message: Message): Thread {
4444
public fun Thread.updateParent(parent: Message): Thread {
4545
// Skip update if [parent] is not related to this Thread
4646
if (this.parentMessageId != parent.id) return this
47+
// Enrich the poll as it might not be present in the event
48+
val poll = parent.poll ?: this.parentMessage.poll
49+
val updatedParent = parent.copy(poll = poll)
4750
return this.copy(
48-
parentMessage = parent,
51+
parentMessage = updatedParent,
4952
deletedAt = parent.deletedAt,
5053
updatedAt = parent.updatedAt ?: this.updatedAt,
5154
)

stream-chat-android-client/src/test/java/io/getstream/chat/android/client/extensions/internal/ThreadExtensionsTests.kt

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ package io.getstream.chat.android.client.extensions.internal
1818

1919
import io.getstream.chat.android.models.ChannelUserRead
2020
import io.getstream.chat.android.models.Message
21+
import io.getstream.chat.android.models.Option
22+
import io.getstream.chat.android.models.Poll
2123
import io.getstream.chat.android.models.Thread
2224
import io.getstream.chat.android.models.ThreadInfo
2325
import io.getstream.chat.android.models.ThreadParticipant
2426
import io.getstream.chat.android.models.User
27+
import io.getstream.chat.android.models.VotingVisibility
2528
import org.amshove.kluent.shouldBeEqualTo
29+
import org.amshove.kluent.shouldNotBeNull
2630
import org.junit.Test
2731
import java.util.Date
2832

@@ -157,6 +161,139 @@ internal class ThreadExtensionsTests {
157161
result shouldBeEqualTo baseThread
158162
}
159163

164+
@Test
165+
fun `updateParent should preserve existing poll when updated parent has no poll`() {
166+
// given
167+
val originalPoll = Poll(
168+
id = "poll1",
169+
name = "Test Poll",
170+
description = "Test Description",
171+
options = listOf(
172+
Option(id = "option1", text = "Option 1"),
173+
Option(id = "option2", text = "Option 2"),
174+
),
175+
votingVisibility = VotingVisibility.PUBLIC,
176+
enforceUniqueVote = true,
177+
maxVotesAllowed = 1,
178+
allowUserSuggestedOptions = false,
179+
allowAnswers = false,
180+
voteCountsByOption = emptyMap(),
181+
votes = emptyList(),
182+
ownVotes = emptyList(),
183+
createdAt = now,
184+
updatedAt = now,
185+
closed = false,
186+
)
187+
val threadWithPoll = baseThread.copy(
188+
parentMessage = parentMessage.copy(poll = originalPoll),
189+
)
190+
val updatedParent = parentMessage.copy(
191+
text = "Updated parent message",
192+
poll = null,
193+
)
194+
195+
// when
196+
val result = threadWithPoll.updateParent(updatedParent)
197+
198+
// then
199+
result.parentMessage.poll.shouldNotBeNull()
200+
result.parentMessage.poll shouldBeEqualTo originalPoll
201+
}
202+
203+
@Test
204+
fun `updateParent should use new poll when updated parent has poll`() {
205+
// given
206+
val originalPoll = Poll(
207+
id = "poll1",
208+
name = "Original Poll",
209+
description = "Original Description",
210+
options = listOf(
211+
Option(id = "option1", text = "Option 1"),
212+
),
213+
votingVisibility = VotingVisibility.PUBLIC,
214+
enforceUniqueVote = true,
215+
maxVotesAllowed = 1,
216+
allowUserSuggestedOptions = false,
217+
allowAnswers = false,
218+
voteCountsByOption = emptyMap(),
219+
votes = emptyList(),
220+
ownVotes = emptyList(),
221+
createdAt = now,
222+
updatedAt = now,
223+
closed = false,
224+
)
225+
val newPoll = Poll(
226+
id = "poll2",
227+
name = "Updated Poll",
228+
description = "Updated Description",
229+
options = listOf(
230+
Option(id = "option1", text = "Option 1"),
231+
Option(id = "option2", text = "Option 2"),
232+
),
233+
votingVisibility = VotingVisibility.ANONYMOUS,
234+
enforceUniqueVote = false,
235+
maxVotesAllowed = 2,
236+
allowUserSuggestedOptions = true,
237+
allowAnswers = true,
238+
voteCountsByOption = mapOf("option1" to 5),
239+
votes = emptyList(),
240+
ownVotes = emptyList(),
241+
createdAt = now,
242+
updatedAt = now,
243+
closed = true,
244+
)
245+
val threadWithPoll = baseThread.copy(
246+
parentMessage = parentMessage.copy(poll = originalPoll),
247+
)
248+
val updatedParent = parentMessage.copy(
249+
text = "Updated parent message",
250+
poll = newPoll,
251+
)
252+
253+
// when
254+
val result = threadWithPoll.updateParent(updatedParent)
255+
256+
// then
257+
result.parentMessage.poll.shouldNotBeNull()
258+
result.parentMessage.poll shouldBeEqualTo newPoll
259+
}
260+
261+
@Test
262+
fun `updateParent should add poll when original parent has no poll but updated parent has poll`() {
263+
// given
264+
val newPoll = Poll(
265+
id = "poll1",
266+
name = "New Poll",
267+
description = "New Description",
268+
options = listOf(
269+
Option(id = "option1", text = "Option 1"),
270+
Option(id = "option2", text = "Option 2"),
271+
),
272+
votingVisibility = VotingVisibility.PUBLIC,
273+
enforceUniqueVote = true,
274+
maxVotesAllowed = 1,
275+
allowUserSuggestedOptions = false,
276+
allowAnswers = false,
277+
voteCountsByOption = emptyMap(),
278+
votes = emptyList(),
279+
ownVotes = emptyList(),
280+
createdAt = now,
281+
updatedAt = now,
282+
closed = false,
283+
)
284+
val updatedParent = parentMessage.copy(
285+
text = "Updated parent message",
286+
poll = newPoll,
287+
)
288+
289+
// when
290+
val result = baseThread.updateParent(updatedParent)
291+
292+
// then
293+
result.parentMessage.poll.shouldNotBeNull()
294+
result.parentMessage.poll shouldBeEqualTo newPoll
295+
}
296+
160297
@Test
161298
fun `upsertReply should add new reply and update related fields`() {
162299
// given

stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,9 @@ internal class EventHandlerSequential(
572572
}
573573
}
574574
is MessageUpdatedEvent -> {
575-
val enrichedMessage = event.message.enrichWithOwnReactions(batch, currentUserId, event.user)
575+
val enrichedMessage = event.message
576+
.enrichWithOwnReactions(batch, currentUserId, event.user)
577+
.enrichWithOwnPoll(batch)
576578
batch.addMessageData(
577579
event.createdAt,
578580
event.cid,
@@ -907,6 +909,11 @@ internal class EventHandlerSequential(
907909
},
908910
)
909911

912+
private fun Message.enrichWithOwnPoll(batch: EventBatchUpdate): Message {
913+
val localMessage = batch.getCurrentMessage(id)
914+
return copy(poll = poll ?: localMessage?.poll)
915+
}
916+
910917
private infix fun UserId.mustBe(currentUserId: UserId?) {
911918
if (this != currentUserId) {
912919
throw InputMismatchException(

stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,10 +549,14 @@ internal class ChannelLogic(
549549
}
550550

551551
is MessageUpdatedEvent -> {
552+
val originalMessage = mutableState.getMessageById(event.message.id)
553+
// Enrich the poll as it might not be present in the event
554+
val poll = event.message.poll ?: originalMessage?.poll
552555
event.message.copy(
553556
replyTo = event.message.replyMessageId
554557
?.let { mutableState.getMessageById(it) }
555558
?: event.message.replyTo,
559+
poll = poll,
556560
).let(::upsertEventMessage)
557561
channelStateLogic.toggleHidden(false)
558562
}

stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/thread/internal/ThreadLogic.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,15 @@ internal class ThreadLogic(
9494
internal fun handleMessageEvents(events: List<HasMessage>) {
9595
val messages = events
9696
.map { event ->
97-
val ownReactions = getMessage(event.message.id)?.ownReactions ?: event.message.ownReactions
97+
val originalMessage = getMessage(event.message.id)
98+
val ownReactions = originalMessage?.ownReactions ?: event.message.ownReactions
99+
// Enrich the poll as might not be present in the event
100+
val poll = event.message.poll ?: originalMessage?.poll
98101
if (event is MessageUpdatedEvent) {
99102
event.message.copy(
100103
replyTo = mutableState.messages.value.firstOrNull { it.id == event.message.replyMessageId },
101104
ownReactions = ownReactions,
105+
poll = poll,
102106
)
103107
} else {
104108
event.message.copy(

stream-chat-android-state/src/test/java/io/getstream/chat/android/state/channel/controller/WhenHandleEvent.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,78 @@ internal class WhenHandleEvent : SynchronizedCoroutineTest {
148148
verify(channelStateLogic, times(2)).upsertMessage(messageUpdateEvent.message)
149149
}
150150

151+
@Test
152+
fun `when message update event arrives, channel should be toggled to not hidden`() = runTest {
153+
val message = randomMessage(
154+
id = randomString(),
155+
user = User(id = "otherUserId"),
156+
silent = false,
157+
showInChannel = true,
158+
)
159+
channelMutableState.setMessages(listOf(message))
160+
161+
val messageUpdateEvent = randomMessageUpdateEvent(message = message)
162+
163+
channelLogic.handleEvent(messageUpdateEvent)
164+
165+
verify(channelStateLogic).toggleHidden(false)
166+
}
167+
168+
@Test
169+
fun `when message update event arrives without poll but original message has poll, poll should be preserved`() = runTest {
170+
val poll = randomPoll()
171+
val originalMessage = randomMessage(
172+
id = randomString(),
173+
user = User(id = "otherUserId"),
174+
silent = false,
175+
showInChannel = true,
176+
poll = poll,
177+
)
178+
channelMutableState.setMessages(listOf(originalMessage))
179+
180+
val updatedMessageWithoutPoll = originalMessage.copy(
181+
text = "Updated text",
182+
poll = null,
183+
)
184+
val messageUpdateEvent = randomMessageUpdateEvent(message = updatedMessageWithoutPoll)
185+
186+
channelLogic.handleEvent(messageUpdateEvent)
187+
188+
verify(channelStateLogic).upsertMessage(
189+
org.mockito.kotlin.argThat { message ->
190+
message.poll == poll && message.text == "Updated text"
191+
},
192+
)
193+
}
194+
195+
@Test
196+
fun `when message update event arrives with poll, event poll should be used`() = runTest {
197+
val originalPoll = randomPoll(id = "poll1")
198+
val originalMessage = randomMessage(
199+
id = randomString(),
200+
user = User(id = "otherUserId"),
201+
silent = false,
202+
showInChannel = true,
203+
poll = originalPoll,
204+
)
205+
channelMutableState.setMessages(listOf(originalMessage))
206+
207+
val eventPoll = randomPoll(id = "poll2")
208+
val updatedMessage = originalMessage.copy(
209+
text = "Updated text",
210+
poll = eventPoll,
211+
)
212+
val messageUpdateEvent = randomMessageUpdateEvent(message = updatedMessage)
213+
214+
channelLogic.handleEvent(messageUpdateEvent)
215+
216+
verify(channelStateLogic).upsertMessage(
217+
org.mockito.kotlin.argThat { message ->
218+
message.poll == eventPoll && message.text == "Updated text"
219+
},
220+
)
221+
}
222+
151223
// Member added event
152224
@Test
153225
fun `when member is added, it should be propagated`(): Unit = runTest {

0 commit comments

Comments
 (0)