Set max lines for 'in reply to' view conditionally (#6612)

* Set max lines for 'in reply to' view conditionally. When there is enough screen space, use 2 lines as before. If the screen space is limited, use a single one.

* Reduce vertical padding for reply-to view in compose

* Add screenshot test with single line in reply to view

* Update screenshots

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Jorge Martin Espinosa 2026-04-23 15:15:52 +02:00 committed by GitHub
parent e036250539
commit 92ee479e91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 274 additions and 205 deletions

View file

@ -24,6 +24,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -64,7 +65,7 @@ internal fun ComposerModeView(
}
is MessageComposerMode.Reply -> {
ReplyToModeView(
modifier = modifier.padding(8.dp),
modifier = modifier.padding(top = 8.dp, start = 8.dp, end = 8.dp),
replyToDetails = composerMode.replyToDetails,
hideImage = composerMode.hideImage,
onResetComposerMode = onResetComposerMode,
@ -120,6 +121,9 @@ private fun EditingModeView(
}
}
// This combination of density DPI and font scale is an approximation to a screen with little space to display the content
private const val MAX_SCALING_VALUE = 3.5f
/**
* https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2019-6286
*/
@ -137,9 +141,14 @@ private fun ReplyToModeView(
.border(1.dp, ElementTheme.colors.separatorPrimary, RoundedCornerShape(6.dp))
.padding(4.dp)
) {
// Larger density DPI and font scale means less space to display the content, so we limit it to 1 line to avoid overflow issues
val currentDensity = LocalDensity.current
val hasLowResolution = currentDensity.density * currentDensity.fontScale >= MAX_SCALING_VALUE
val maxReplyContentLines = if (hasLowResolution) 1 else 2
InReplyToView(
inReplyTo = replyToDetails,
hideImage = hideImage,
maxLines = maxReplyContentLines,
modifier = Modifier.weight(1f),
)
Icon(

View file

@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -40,6 +41,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
@ -50,6 +52,7 @@ import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
@ -66,10 +69,14 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.IconColorButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventOrTransactionId
import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent
import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetails
import io.element.android.libraries.matrix.ui.messages.reply.InReplyToDetailsProvider
import io.element.android.libraries.matrix.ui.messages.reply.aProfileDetailsReady
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag
import io.element.android.libraries.textcomposer.components.SendButtonIcon
@ -181,7 +188,7 @@ fun TextComposer(
placeholder = placeholder,
registerStateUpdates = true,
modifier = Modifier
.padding(top = 6.dp, bottom = 6.dp)
.padding(top = 4.dp, bottom = 6.dp)
.fillMaxWidth(),
style = ElementRichTextEditorStyle.composerStyle(hasFocus = state.richTextEditorState.hasFocus),
resolveMentionDisplay = resolveMentionDisplay,
@ -651,10 +658,14 @@ private fun TextInputBox(
composerMode = composerMode,
onResetComposerMode = onResetComposerMode,
)
} else {
// Top padding for the message composer box
Spacer(Modifier.height(4.dp))
}
Box(
modifier = Modifier
.padding(top = 4.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
.padding(top = 1.dp, bottom = 4.dp, start = 12.dp, end = 12.dp)
.then(Modifier.testTag(TestTags.textEditor)),
contentAlignment = Alignment.CenterStart,
) {
@ -664,7 +675,7 @@ private fun TextInputBox(
Icon(
modifier = Modifier
.clickable { showBottomSheet = true }
.padding(horizontal = 8.dp, vertical = 4.dp)
.padding(start = 8.dp, end = 8.dp, top = 4.dp, bottom = 4.dp)
.align(Alignment.CenterEnd),
imageVector = CompoundIcons.InfoSolid(),
tint = ElementTheme.colors.iconCriticalPrimary,
@ -983,6 +994,40 @@ internal fun TextComposerVoiceNotEncryptedPreview() = ElementPreview {
}
}
@Preview
@Composable
internal fun TextComposerScaledDensityWithReplyPreview() {
ElementPreview {
CompositionLocalProvider(
LocalDensity provides Density(
density = 3f,
fontScale = 1.25f,
),
) {
val replyToDetails = InReplyToDetails.Ready(
eventId = EventId("\$1234"),
senderId = UserId("@alice:example.com"),
senderProfile = aProfileDetailsReady(),
eventContent = MessageContent(
body = "Message which are being replied, and which was long enough to be displayed on two lines (only!).",
inReplyTo = null,
isEdited = false,
threadInfo = null,
type = TextMessageType("Message which are being replied, and which was long enough to be displayed on two lines (only!).", null)
),
textContent = "Message which are being replied, and which was long enough to be displayed on two lines (only!).",
)
Box(modifier = Modifier.width(480.dp).height(120.dp)) {
ATextComposer(
state = aTextEditorStateMarkdown(initialText = "", initialFocus = true),
voiceMessageState = VoiceMessageState.Idle,
composerMode = MessageComposerMode.Reply(replyToDetails, hideImage = false),
)
}
}
}
}
@Composable
private fun <T> PreviewColumn(
items: ImmutableList<T>,

View file

@ -82,7 +82,7 @@ fun MarkdownTextInput(
AndroidView(
modifier = Modifier
.padding(top = 6.dp, bottom = 6.dp)
.padding(top = 5.dp, bottom = 6.dp)
.fillMaxWidth(),
factory = { context ->
MarkdownEditText(context).apply {