Scope message cell ripple to the bubble shape#6425
Scope message cell ripple to the bubble shape#6425
Conversation
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.
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
WalkthroughThis PR threads an ChangesMessage UI InteractionSource Support
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
stream-chat-android-compose/api/stream-chat-android-compose.apistream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/FileAttachmentContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/GiphyAttachmentContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/attachments/content/LinkAttachmentContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactory.ktstream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/theme/ChatComponentFactoryParams.kt
| Column( | ||
| modifier = Modifier.combinedClickable( | ||
| interactionSource = remember(::MutableInteractionSource), | ||
| indication = ripple(), | ||
| onClick = {}, | ||
| onLongClick = { onLongItemClick(message) }, | ||
| ), |
There was a problem hiding this comment.
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).
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.
The Column-level combinedClickable inside DefaultMessageRegularContent
and the inner PollMessageContent consumed taps with onClick = {} and
fired a raw onLongItemClick(message) without haptic. Two regressions
followed: tapping a thread-start message inside the bubble no longer
opened the thread (the cell's onClick was shadowed), and bubble
long-press lost the haptic feedback that the cell triggered for
avatar-gap presses.
Hoist onItemClick and onItemLongClick as named lambdas in
MessageContainer, both wrapping the canOpenThread / canOpenActions
gates and the haptic call. Plumb them through MessageContentParams,
MessageRegularContentParams, the public DefaultMessageContent /
RegularMessageContent / MessageContent / PollMessageContent
composables, and the factory defaults so the bubble Column can call
them directly.
This keeps canOpenActions in a single place (MessageContainer) and
removes the LocalHapticFeedback usage from MessageContent.kt and
PollMessageContent.kt: the bubble Column no longer needs to know
about action-permission rules or haptic policy. The inner private
PollMessageContent overload also drops its now-unused
onLongItemClick: (Message) -> Unit parameter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b87f3e0 to
25e0c5b
Compare
|



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:
Column(inDefaultMessageRegularContent) in acombinedClickablewith a bounded ripple. Position-aware ripple, clipped to the bubble shape via the surroundingSurface.combinedClickablekeepsindication = nulland forwards itsMutableInteractionSourceto the bubble throughMessageBubbleParams.interactionSourceandMessageContentParams.interactionSource. The bubble's factory default applies an unbounded ripple driven by that source. Press positions stay in cell-local coords, so we usebounded = falseto avoid a clipping mismatch — the ripple emanates from the bubble center.Attachment ripple fixes. Giphy, file, link and quoted-message clickables had
indication = null. Addedripple()to align them with the image attachment, which already had a bounded ripple.Important
Decisions worth flagging for review
interactionSourcetoMessageBubbleParamsandMessageContentParamsmakes the override path discoverable: integrators overridingChatComponentFactory.MessageBubblecan readparams.interactionSourceand decide what to do with it. The CompositionLocal approach we tried earlier hides the dependency.Known limitation
Text messages whose body contains a URL, email, or
@mentiondon't ripple on text-character taps. The internalClickableText(inMessageText.kt) usespointerInput { 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
LinkAnnotationAPI (AnnotatedStringwithLinkAnnotation.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
Screen_recording_20260507_165648.webm
Screen_recording_20260507_165559.webm
Testing
Summary by CodeRabbit