Fix long messages not being clickable (#6356)

* Fix long messages not being clickable

As @bmarty found out, `clip = true` causes the click event to be ignored in some cases. Since we have the shape we want to draw and we're using a custom `onDraw` modifier anyway to cut-out part of the path, we can just draw everything using the modifier and avoid using `clip = true`.

This seems to fix the issue.

* Fix clipping of images or other items that cover the bubble

* Fix borders being displayed for contents

* Extract the layer drawing logic into `drawInLayer` to simplify the inlined code. Remove redundant code, those changes are now in the `drawInLayer` block

* Workaround for lint issue: it seems like detekt can't properly detect usages in content receivers

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa
2026-03-23 18:11:55 +01:00
committed by GitHub
parent 13bbd24df1
commit a2d9f241dd
2 changed files with 70 additions and 23 deletions

View File

@@ -22,14 +22,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@@ -49,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.ui.utils.graphics.drawInLayer
import io.element.android.libraries.ui.utils.time.isTalkbackActive
private val BUBBLE_RADIUS = 12.dp
@@ -78,32 +77,45 @@ fun MessageEventBubble(
.onKeyboardContextMenuAction(onLongClick)
}
val cutTopStart = state.cutTopStart
// Ignore state.isHighlighted for now, we need a design decision on it.
val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine)
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
BoxWithConstraints(
modifier = modifier
.graphicsLayer {
shape = bubbleShape
clip = true
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
drawRect(backgroundBubbleColor)
drawContent()
if (state.cutTopStart) {
drawCircle(
color = Color.Black,
center = Offset(
x = if (isRtl) size.width else 0f,
y = yOffsetPx,
),
radius = radiusPx,
blendMode = BlendMode.Clear,
)
.drawWithCache {
// Calculate the outline of the background and cache it
val outline = bubbleShape.createOutline(size, layoutDirection, this)
onDrawWithContent {
// Draw the contents in a layer to be able to clip them with the same outline
// For some reason, doing this clipping outside a layer messes up with the touch events
drawInLayer(
composingStrategy = CompositingStrategy.Offscreen,
outline = outline,
clip = true,
) {
// Draw the background first, so that it's behind the content
drawRect(backgroundBubbleColor)
// Then draw the content on top of it
drawContent()
// And then clip the top start corner if needed to make room for the avatar
if (cutTopStart) {
drawCircle(
color = Color.Black,
center = Offset(
x = if (layoutDirection == LayoutDirection.Rtl) size.width else 0f,
y = yOffsetPx,
),
radius = radiusPx,
blendMode = BlendMode.Clear,
)
}
}
}
},
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case

View File

@@ -0,0 +1,35 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.ui.utils.graphics
import androidx.compose.ui.draw.CacheDrawScope
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.layer.CompositingStrategy
import androidx.compose.ui.graphics.layer.drawLayer
import androidx.compose.ui.graphics.layer.setOutline
/**
* Draws the content of [recordBlock] in a separate layer, which can be customized using [composingStrategy], [outline] and [clip].
*/
context(scope: androidx.compose.ui.graphics.drawscope.DrawScope)
fun CacheDrawScope.drawInLayer(
composingStrategy: CompositingStrategy = CompositingStrategy.Auto,
outline: Outline? = null,
clip: Boolean = false,
recordBlock: ContentDrawScope.() -> Unit,
) {
val layer = obtainGraphicsLayer().apply {
this.compositingStrategy = composingStrategy
this.clip = clip
outline?.let { this.setOutline(it) }
record(block = recordBlock)
}
scope.drawLayer(layer)
}