Skip to content

Scope message cell ripple to the bubble shape#6425

Draft
andremion wants to merge 3 commits intodevelopfrom
fix/sdk-message-cell-ripple-scope
Draft

Scope message cell ripple to the bubble shape#6425
andremion wants to merge 3 commits intodevelopfrom
fix/sdk-message-cell-ripple-scope

Conversation

@andremion
Copy link
Copy Markdown
Contributor

@andremion andremion commented May 7, 2026

Goal

Move the ripple from the message cell to the bubble, while keeping the cell-wide hit area.

Implementation

Two ripple paths, one per click region:

  • Inside the bubble — wrap the message content Column (in DefaultMessageRegularContent) in a combinedClickable with a bounded ripple. Position-aware ripple, clipped to the bubble shape via the surrounding Surface.
  • Outside the bubble (avatar gap, cell row) — the cell's existing combinedClickable keeps indication = null and forwards its MutableInteractionSource to the bubble through MessageBubbleParams.interactionSource and MessageContentParams.interactionSource. The bubble's factory default applies an unbounded ripple driven by that source. Press positions stay in cell-local coords, so we use bounded = false to avoid a clipping mismatch — the ripple emanates from the bubble center.

Attachment ripple fixes. Giphy, file, link and quoted-message clickables had indication = null. Added ripple() to align them with the image attachment, which already had a bounded ripple.

Important

Decisions worth flagging for review

  • Two ripple paths instead of one. The Column-level click only fires for taps inside the bubble; avatar-gap presses go to the cell. Without the params plumbing, those presses had no visual feedback. The two paths cover non-overlapping regions and never double-fire (gesture consumption rules).
  • Params plumbing instead of CompositionLocal. Adding interactionSource to MessageBubbleParams and MessageContentParams makes the override path discoverable: integrators overriding ChatComponentFactory.MessageBubble can read params.interactionSource and decide what to do with it. The CompositionLocal approach we tried earlier hides the dependency.
  • Why ripple at all. Aligns with WhatsApp, Material guidance, and the Flutter SDK — ripple is touch acknowledgement, not "this has a tap action". Telegram and iMessage skip it; we follow the WhatsApp pattern for cross-platform parity within Stream's SDKs.

Known limitation

Text messages whose body contains a URL, email, or @mention don't ripple on text-character taps. The internal ClickableText (in MessageText.kt) uses pointerInput { detectTapGestures(...) } to route tap-on-link to the correct character — that consumer blocks the parent Column from receiving the press. Link clicks still work; long-press still opens actions with haptic feedback.

The proper fix is to migrate the link/mention handling to Compose Foundation's LinkAnnotation API (AnnotatedString with LinkAnnotation.Url / LinkAnnotation.Clickable). That API lets non-link taps propagate to the parent and pick up the bubble ripple. Tracked as a follow-up.

🎨 UI Changes

Before After
Screen_recording_20260507_165648.webm
Screen_recording_20260507_165559.webm

Testing

  1. Open any channel in the Compose sample.
  2. Long-press on a plain text message (no links).
    • Expected: bounded ripple inside the bubble, position-aware (matches the touch point), confined to the bubble shape.
  3. Long-press in the avatar gap (cell area outside the bubble).
    • Expected: unbounded ripple emanating from the bubble center; long-press still opens the action menu with haptic feedback.
  4. Long-press on an image, file, giphy, link preview, or quoted-message preview.
    • Expected: each renders its own bounded ripple inside its content shape.
  5. Tap on a link inside a text message.
    • Expected: opens the link.
  6. Long-press on a text message that has a link or mention.
    • Expected: action menu opens, haptic fires; no visible ripple on the text characters (known limitation, see above).

Summary by CodeRabbit

  • New Features
    • Added visual ripple feedback to file, Giphy, and link attachment interactions.
    • Improved message interaction feedback with ripple effects throughout the messaging interface.
    • Enhanced poll message interaction support.

andremion added 3 commits May 7, 2026 15:34
Image attachments already render a ripple on tap via their
combinedClickable. Giphy, file rows, link previews and the quoted-
message preview block had indication = null, leaving them without
visual feedback. Match the image-attachment pattern across all four
so every interactive surface inside a message bubble ripples
consistently on press.
Wrap the message-content Column inside DefaultMessageRegularContent
with combinedClickable + ripple(). The Column owns the click + ripple
for the entire bubble interior (text, spacer, and any space around
inner attachments). Inner attachment clickables (image, file, giphy,
link, quoted) still consume their own taps and ripple in their own
bounds.

This replaces the earlier params-based bubble ripple (which had a
position-translation issue between the cell's interaction source and
the bubble's local coords). With the click and the ripple at the
same layout node, press positions are captured in Column-local coords
and the ripple renders correctly regardless of message alignment or
bubble width.
Restore the ripple feedback on the bubble when the user long-presses
in the avatar gap (or any cell area outside the bubble). The
column-level clickable inside DefaultMessageRegularContent only fires
for taps inside the bubble; cell-area presses go to MessageContainer's
combinedClickable, which has indication = null. Without forwarding the
cell's interaction source, those presses had no visual feedback.

Add interactionSource to MessageBubbleParams and MessageContentParams.
The cell hoists its MutableInteractionSource and threads it via
MessageContainer -> factory.MessageContent -> DefaultMessageContent ->
RegularMessageContent / PollMessageContent -> factory.MessageBubble.
The MessageBubble factory default applies clip(shape).indication(
source, ripple(bounded = false)) when the source is non-null,
synchronised with the cell's press state.

Two ripple paths now coexist by design:
- Tap inside the bubble: column's combinedClickable consumes,
  column-level bounded ripple fires (position-aware).
- Tap in avatar gap: cell's combinedClickable handles, the cell's
  source emits, bubble's params indication renders an unbounded
  ripple from the bubble centre.

Both fire on the correct trigger; no double rippling thanks to
gesture consumption rules.
@andremion andremion added the pr:improvement Improvement label May 7, 2026
@andremion
Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 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 (or ignored for dependabot PRs).

🎉 Great job! This PR is ready for review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 7, 2026

SDK Size Comparison 📏

SDK Before After Difference Status
stream-chat-android-client 5.82 MB 5.82 MB 0.00 MB 🟢
stream-chat-android-ui-components 11.02 MB 11.02 MB 0.00 MB 🟢
stream-chat-android-compose 12.38 MB 12.38 MB 0.00 MB 🟢

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Walkthrough

This PR threads an InteractionSource parameter through the message UI hierarchy to enable coordinated ripple visual feedback. The interaction source is created in MessageContainer, threaded through content composables, and applied as unbounded ripples in both the message bubble factory and attachment content items.

Changes

Message UI InteractionSource Support

Layer / File(s) Summary
Data Contracts
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
MessageBubbleParams and MessageContentParams add nullable interactionSource property with KDoc describing ripple synchronization behavior when non-null.
Factory Ripple Implementation
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
MessageBubble rendering conditionally applies a clipped, unbounded ripple indication modifier when params.interactionSource is provided.
Container Creation and Threading
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt
MessageContainer creates a remembered MutableInteractionSource and threads it through DefaultMessageContent and RegularMessageContent to MessageBubbleParams for both poll and regular messages.
Poll Message Content
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt
Public PollMessageContent composable accepts optional interactionSource parameter and forwards it to MessageBubbleParams in both success and error rendering paths.
Message Content UI
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt
DefaultMessageRegularContent wraps message column with combinedClickable using remembered MutableInteractionSource and ripple indication; quoted message click handler also uses default ripple.
Attachment Content Ripple
stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/*AttachmentContent.kt
FileAttachmentContent, GiphyAttachmentContent, and LinkAttachmentContent update combinedClickable to use ripple() indication instead of disabling indications.
Public API Surface
stream-chat-android-compose/api/stream-chat-android-compose.api
API surface declarations updated to reflect new interactionSource parameters in composables and data classes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A ripple flows through messages bright,
From top to bottom, left to right,
Each press now dances with the state,
Touch feedback makes the UI great! 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and concisely describes the main change: scoping the message cell ripple effect to the bubble shape, which is the core objective of this PR.
Description check ✅ Passed The description comprehensively covers all template sections: Goal, Implementation, UI Changes with videos, Testing steps, and contributor/reviewer checklists. It includes detailed rationale for design decisions and known limitations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/sdk-message-cell-ripple-scope

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt`:
- Around line 183-189: The Column's combinedClickable currently uses a no-op
onClick which swallows taps; replace that no-op by invoking the parent click
intent so bubble taps still trigger the MessageContainer thread-open behavior.
In MessageContent.kt update the combinedClickable onClick to call the parent
handler that's passed into this composable (e.g., invoke the onMessageClick /
onThreadOpen callback or forward the existing click lambda used by
MessageContainer) instead of {}, keeping onLongClick as
onLongItemClick(message).
🪄 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: d453df3f-1b17-4f76-9aaa-fe5ec0fdb5bc

📥 Commits

Reviewing files that changed from the base of the PR and between cf0b562 and b87f3e0.

📒 Files selected for processing (9)
  • stream-chat-android-compose/api/stream-chat-android-compose.api
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.kt
  • stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt

Comment on lines +183 to +189
Column(
modifier = Modifier.combinedClickable(
interactionSource = remember(::MutableInteractionSource),
indication = ripple(),
onClick = {},
onLongClick = { onLongItemClick(message) },
),
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 | 🟠 Major | 🏗️ Heavy lift

No-op child click is swallowing parent thread-open taps.

The new combinedClickable consumes taps inside the bubble (onClick = {}), so the parent click handler in MessageContainer no longer receives those events. For thread-start messages, tapping the bubble can stop opening the thread.

💡 Proposed direction
- internal fun DefaultMessageRegularContent(
+ internal fun DefaultMessageRegularContent(
     message: Message,
     currentUser: User?,
     messageAlignment: MessageAlignment = MessageAlignment.Start,
     onLongItemClick: (Message) -> Unit,
+    onItemClick: () -> Unit = {},
     onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {},
     onQuotedMessageClick: (Message) -> Unit,
     onUserMentionClick: (User) -> Unit = {},
     onLinkClick: ((Message, String) -> Unit)? = null,
 ) {
@@
-        modifier = Modifier.combinedClickable(
+        modifier = Modifier.combinedClickable(
             interactionSource = remember(::MutableInteractionSource),
             indication = ripple(),
-            onClick = {},
+            onClick = onItemClick,
             onLongClick = { onLongItemClick(message) },
         ),

Then pass the existing parent click intent (thread-open logic) into this callback path so bubble taps keep expected behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.kt`
around lines 183 - 189, The Column's combinedClickable currently uses a no-op
onClick which swallows taps; replace that no-op by invoking the parent click
intent so bubble taps still trigger the MessageContainer thread-open behavior.
In MessageContent.kt update the combinedClickable onClick to call the parent
handler that's passed into this composable (e.g., invoke the onMessageClick /
onThreadOpen callback or forward the existing click lambda used by
MessageContainer) instead of {}, keeping onLongClick as
onLongItemClick(message).

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 7, 2026

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

Labels

pr:improvement Improvement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant