Skip to content

Fix image flickering on message updates#6307

Open
VelikovPetar wants to merge 2 commits intov7from
bug/fix_image_flciker_on_message_updates
Open

Fix image flickering on message updates#6307
VelikovPetar wants to merge 2 commits intov7from
bug/fix_image_flciker_on_message_updates

Conversation

@VelikovPetar
Copy link
Copy Markdown
Contributor

@VelikovPetar VelikovPetar commented Mar 30, 2026

Goal

Image attachments flicker on message updates (reactions, pins and similar...)
The reason for this is that responses/events which update existing messages with image attachments, also deliver image attachments with a differently signed imageUrl. The fix re-introduces the AttachmentUrlValidator logic in the new ChannelLogicImpl (already existing in the ChannelLogicLegacyImpl).

Note: This PR just introduces the old mechanism for by-passing this issue. We should explore different approaches for handling this (perhaps on image caching level), as with this 'fix', we basically store 'outdated' image data in the message list.

Implementation

  • Add attachmentUrlValidator.updateValidAttachmentsUrl invocations on every message update in ChannelLogicImpl

🎨 UI Changes

Before After
flicker_reaction_before.mp4
flicker_reaction_after.mp4

Testing

  1. React / Pin / Unpin (or any update to an existing message)
  2. The image should re-load (flicker)

Summary by CodeRabbit

  • New Features

    • Implemented attachment URL preservation during message updates to prevent unnecessary image and attachment reloads when message content is refreshed or synchronized.
  • Bug Fixes

    • Enhanced message state management to maintain attachment integrity and prevent re-rendering of cached attachments during event-based message updates.
  • Refactor

    • Restructured internal message update APIs to support URL-aware field preservation across state transitions.

@VelikovPetar VelikovPetar added the pr:bug Bug fix label Mar 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

PR checklist ✅

All required conditions are satisfied:

  • Title length is OK (or ignored by label).
  • At least one pr: label exists.
  • Sections ### Goal, ### Implementation, and ### Testing are filled.

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 30, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.25 MB 5.68 MB 0.43 MB 🟡
stream-chat-android-ui-components 10.60 MB 10.99 MB 0.39 MB 🟡
stream-chat-android-compose 12.81 MB 12.11 MB -0.70 MB 🚀

@VelikovPetar VelikovPetar marked this pull request as ready for review March 30, 2026 09:57
@VelikovPetar VelikovPetar requested a review from a team as a code owner March 30, 2026 09:57
@sonarqubecloud
Copy link
Copy Markdown

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Walkthrough

This PR introduces attachment URL preservation across message update operations. Changes include making a helper method internal for broader module access, adding an AttachmentUrlValidator dependency to ChannelStateImpl, updating message upsert/update method signatures with a preserveAttachmentUrls boolean flag, exposing a new updateMessageFromEvent function for event-based message merging, and adding comprehensive test coverage for attachment URL handling.

Changes

Cohort / File(s) Summary
Attachment URL Validator
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/message/attachments/internal/AttachmentUrlValidator.kt
Changed updateValidAttachmentsUrl method visibility from private to internal to allow access from other files in the module scope.
Message Event Handling
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImpl.kt
Updated reaction-handling logic to use state.updateMessageFromEvent(message) with a two-parameter merge lambda instead of state.updateMessageById, preserving ownReactions from prior message state.
Channel Logic
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImpl.kt
Updated calls to upsertMessages and upsertCachedLatestMessages to pass preserveAttachmentUrls = true in search and pagination paths. Minor comment updates for clarity without behavioral changes.
Channel State
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImpl.kt
Injected AttachmentUrlValidator dependency and refactored message upsert/update paths to preserve attachment URLs. Updated signatures: upsertMessages(messages, preserveAttachmentUrls = false), upsertCachedLatestMessages(messages, preserveAttachmentUrls = false). Added updateMessageFromEvent(eventMessage, enrich) for event-driven message merging with URL preservation.
List Utilities
stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/utils/internal/List.kt
Added optional update: (old: T) -> T parameter to upsertSortedBounded to allow custom update logic when upserting existing elements.
Test Files: Mocking & Verification
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/channel/controller/WhenHandleEvent.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/legacy/ChannelStateLogicTest.kt
Updated Mockito matchers from untyped any() to typed matchers (any<List<Message>>(), any<Map<String, Message>>()) for better type safety in stubbing attachment validator calls.
Test Files: API Updates
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImplTest.kt, stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImplTest.kt
Updated test expectations to match new method signatures: capture two-parameter lambdas for updateMessageFromEvent, verify additional preserveAttachmentUrls parameter in upsertMessages and upsertCachedLatestMessages calls.
Test Files: New Coverage
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplMessagesTest.kt
Added new AttachmentUrlPreservation test suite with helper createMessageWithAttachment to verify URL preservation behavior across upsertMessage, upsertMessages, updateMessage, and updateMessageFromEvent operations.

Sequence Diagram

sequenceDiagram
    actor Client
    participant ChannelEventHandler as Event Handler
    participant ChannelLogic as Channel Logic
    participant ChannelState as Channel State
    participant Validator as AttachmentUrlValidator
    participant Messages as Message Store

    Client->>ChannelEventHandler: Reaction Event
    ChannelEventHandler->>ChannelLogic: Update message with reaction
    ChannelLogic->>ChannelState: upsertMessages(messages, preserveAttachmentUrls=true)
    ChannelState->>Validator: updateValidAttachmentsUrl(newMessages, oldMessages)
    Validator->>Validator: Preserve URL signatures from old state
    Validator->>ChannelState: Return messages with preserved URLs
    ChannelState->>Messages: Store updated message
    ChannelState-->>Client: Message state updated with reaction + preserved URLs

    Client->>ChannelEventHandler: Message Update Event
    ChannelEventHandler->>ChannelState: updateMessageFromEvent(message, enrich)
    ChannelState->>Validator: Check attachment URLs in old message
    Validator->>ChannelState: Return message with preserved URLs
    ChannelState->>ChannelState: Apply enrich lambda for additional fields
    ChannelState->>Messages: Store final merged message
    ChannelState-->>Client: Message state updated with merged fields
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 With careful hops through message streams so bright,
We weave attachment URLs tight,
No more reloads when updates bloom,
URL signatures preserved in their room!
State flows true, and reactions thrive—
Our messages now perfectly alive!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes Goal, Implementation, UI Changes (with before/after videos), and Testing sections. However, it is missing the required Contributor Checklist and Reviewer Checklist sections. Complete the Contributor Checklist and Reviewer Checklist sections to fully conform to the repository's PR template requirements.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change—fixing image flickering on message updates—which aligns with the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bug/fix_image_flciker_on_message_updates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplMessagesTest.kt (1)

786-899: Consider adding one multi-attachment regression case.

A case with 2+ image attachments (including reordered incoming attachments) would harden this against index/order-based preservation regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplMessagesTest.kt`
around lines 786 - 899, Add a regression test in ChannelStateImplMessagesTest
that covers messages with multiple image attachments (use createMessage(...) and
build attachments via Attachment(...)), set the original message with at least
two attachments where one attachment uses streamCdnImageUrl, then upsert an
updated message whose attachments are reordered and have newStreamCdnImageUrl
values; call channelState.upsertMessage (and/or channelState.upsertMessages with
preserveAttachmentUrls = true) and assert that the attachment that originally
had the valid streamCdnImageUrl still preserves that URL after the upsert while
other attachments reflect the updated URLs, to guard against index/order-based
preservation regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImpl.kt`:
- Around line 422-425: The preserved-URL Message produced by
preserveAttachmentUrls(...) is only applied inside internal lists and never
returned to callers, so external code (e.g., updateQuotedMessageReferences) can
still see and propagate the original re-signed imageUrl; modify updateMessage
and the other update path (the one at lines indicated in the review) so that
updateMessageById(...) returns the merged/updated Message (apply
preserveAttachmentUrls and return that Message) and propagate that returned
Message out to callers, or alternatively call preserveAttachmentUrls again
inside updateQuotedMessageReferences(...) to ensure quoted/secondary fan-out
always receives the URL-preserved Message; reference the functions
updateMessage, updateMessageById, preserveAttachmentUrls, and
updateQuotedMessageReferences when making the change.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/channel/controller/WhenHandleEvent.kt`:
- Around line 101-106: The test stubs for
attachmentUrlValidator.updateValidAttachmentsUrl(...) are ineffective because
attachmentUrlValidator is not injected into the SUT (ChannelLogicLegacyImpl) and
channelStateLogic is a mock; fix by wiring the real or properly injected
attachmentUrlValidator into the ChannelLogicLegacyImpl instance used by the
tests (or replace channelStateLogic mock with a spy that delegates to the
provided attachmentUrlValidator), then remove the disconnected whenever(...)
stubs and instead let the SUT call the real
attachmentUrlValidator.updateValidAttachmentsUrl(...) (or verify via the spy) so
the tests exercise the actual URL-preservation logic.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImplTest.kt`:
- Around line 595-598: The test currently calls the captured lambda with the
same Message twice, which masks bugs where the implementation might ignore the
`new` parameter; change the captor invocation to pass two distinct Message
instances (e.g., `oldMessage` and `newMessage`) and then assert that
`result.ownReactions` matches the `oldMessage`'s ownReactions and that at least
one field specific to `newMessage` (such as text or id) is present in the result
to ensure `new` is being used; apply the same change to the other similar
assertions that use the argumentCaptor with updateMessageFromEvent (the blocks
around the other mentioned occurrences).

---

Nitpick comments:
In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplMessagesTest.kt`:
- Around line 786-899: Add a regression test in ChannelStateImplMessagesTest
that covers messages with multiple image attachments (use createMessage(...) and
build attachments via Attachment(...)), set the original message with at least
two attachments where one attachment uses streamCdnImageUrl, then upsert an
updated message whose attachments are reordered and have newStreamCdnImageUrl
values; call channelState.upsertMessage (and/or channelState.upsertMessages with
preserveAttachmentUrls = true) and assert that the attachment that originally
had the valid streamCdnImageUrl still preserves that URL after the upsert while
other attachments reflect the updated URLs, to guard against index/order-based
preservation regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9102003d-6977-47db-a13e-c66d25acdc30

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6ceb0 and 55d9cb4.

📒 Files selected for processing (10)
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/message/attachments/internal/AttachmentUrlValidator.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImpl.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImpl.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImpl.kt
  • stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/utils/internal/List.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/channel/controller/WhenHandleEvent.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImplTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelLogicImplTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/legacy/ChannelStateLogicTest.kt
  • stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImplMessagesTest.kt

Comment on lines 422 to +425
fun updateMessage(message: Message) {
updateMessageById(message.id) { message }
updateMessageById(message.id) { old ->
preserveAttachmentUrls(message, old)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The URL-preserved Message never escapes these helpers.

Both update paths only apply preserveAttachmentUrls(...) inside the internal lists. Callers still hold the raw event payload, so any secondary fan-out can reintroduce the re-signed imageUrl; MessageUpdatedEvent -> updateQuotedMessageReferences(...) is the current example, which means quoted reply previews can still flicker. Return the merged Message, or preserve again inside updateQuotedMessageReferences.

Also applies to: 457-459

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-client/src/main/java/io/getstream/chat/android/client/internal/state/plugin/state/channel/internal/ChannelStateImpl.kt`
around lines 422 - 425, The preserved-URL Message produced by
preserveAttachmentUrls(...) is only applied inside internal lists and never
returned to callers, so external code (e.g., updateQuotedMessageReferences) can
still see and propagate the original re-signed imageUrl; modify updateMessage
and the other update path (the one at lines indicated in the review) so that
updateMessageById(...) returns the merged/updated Message (apply
preserveAttachmentUrls and return that Message) and propagate that returned
Message out to callers, or alternatively call preserveAttachmentUrls again
inside updateQuotedMessageReferences(...) to ensure quoted/secondary fan-out
always receives the URL-preserved Message; reference the functions
updateMessage, updateMessageById, preserveAttachmentUrls, and
updateQuotedMessageReferences when making the change.

Comment on lines +101 to +106
whenever(
attachmentUrlValidator.updateValidAttachmentsUrl(
any<List<Message>>(),
any<Map<String, Message>>(),
),
) doAnswer { invocation ->
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

These attachmentUrlValidator stubs are disconnected from the SUT.

attachmentUrlValidator is never passed to ChannelLogicLegacyImpl, and channelStateLogic is a mock, so changing updateValidAttachmentsUrl(...) stubbing here doesn't affect the exercised code. These tests still won't catch regressions in legacy URL-preservation wiring until the real collaborator is injected.

Also applies to: 132-137, 286-291, 342-347, 382-387

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/channel/controller/WhenHandleEvent.kt`
around lines 101 - 106, The test stubs for
attachmentUrlValidator.updateValidAttachmentsUrl(...) are ineffective because
attachmentUrlValidator is not injected into the SUT (ChannelLogicLegacyImpl) and
channelStateLogic is a mock; fix by wiring the real or properly injected
attachmentUrlValidator into the ChannelLogicLegacyImpl instance used by the
tests (or replace channelStateLogic mock with a spy that delegates to the
provided attachmentUrlValidator), then remove the disconnected whenever(...)
stubs and instead let the SUT call the real
attachmentUrlValidator.updateValidAttachmentsUrl(...) (or verify via the spy) so
the tests exercise the actual URL-preservation logic.

Comment on lines +595 to 598
val captor = argumentCaptor<(Message, Message) -> Message>()
verify(state).updateMessageFromEvent(eq(message), captor.capture())
val result = captor.firstValue(message, message)
assertEquals(ownReactions, result.ownReactions)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use different old and new messages in these captor assertions.

Each test calls the captured (old, new) lambda with the same Message instance twice, so it still passes if the implementation ignores new and just returns old. Pass distinct inputs and assert one field from new plus ownReactions from old.

Also applies to: 619-622, 643-646

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@stream-chat-android-client/src/test/java/io/getstream/chat/android/client/internal/state/plugin/logic/channel/internal/ChannelEventHandlerImplTest.kt`
around lines 595 - 598, The test currently calls the captured lambda with the
same Message twice, which masks bugs where the implementation might ignore the
`new` parameter; change the captor invocation to pass two distinct Message
instances (e.g., `oldMessage` and `newMessage`) and then assert that
`result.ownReactions` matches the `oldMessage`'s ownReactions and that at least
one field specific to `newMessage` (such as text or id) is present in the result
to ensure `new` is being used; apply the same change to the other similar
assertions that use the argumentCaptor with updateMessageFromEvent (the blocks
around the other mentioned occurrences).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant