From 26ef42523422dc4fec15db6f416b9e9d92c4af89 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jan 2026 15:43:27 +0100 Subject: [PATCH 1/8] A11Y: ensure a11y focus is not lost and reset to the back button when the user start playing a pending voice message. --- .../components/VoiceMessagePreview.kt | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt index d893979889..8ca90843a4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessagePreview.kt @@ -67,22 +67,12 @@ internal fun VoiceMessagePreview( .heightIn(26.dp), verticalAlignment = Alignment.CenterVertically, ) { - if (isPlaying) { - PlayerButton( - type = PlayerButtonType.Pause, - onClick = onPauseClick, - enabled = isInteractive, - ) - } else { - PlayerButton( - type = PlayerButtonType.Play, - onClick = onPlayClick, - enabled = isInteractive - ) - } - + PlayerButton( + type = if (isPlaying) PlayerButtonType.Pause else PlayerButtonType.Play, + onClick = if (isPlaying) onPauseClick else onPlayClick, + enabled = isInteractive, + ) Spacer(modifier = Modifier.width(8.dp)) - Text( text = time.formatShort(), color = ElementTheme.colors.textSecondary, @@ -90,9 +80,7 @@ internal fun VoiceMessagePreview( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Spacer(modifier = Modifier.width(12.dp)) - WaveformPlaybackView( modifier = Modifier .weight(1f) From 1b217d464936eded43a5318f453440f555a3c27b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jan 2026 10:01:23 +0100 Subject: [PATCH 2/8] A11Y: ensure a11y focus is not lost and reset to the back button when the user use the keyboard to focus the send button and press the space bar to perform a click. --- .../libraries/textcomposer/TextComposer.kt | 62 ++++++++++++--- .../textcomposer/components/SendButton.kt | 76 +++++++++--------- .../components/VoiceMessageRecorderButton.kt | 78 +++++++------------ 3 files changed, 114 insertions(+), 102 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index f2070c9610..200c6e63fb 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -39,6 +39,8 @@ import androidx.compose.runtime.setValue 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.LocalHapticFeedback import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.SemanticsPropertyReceiver @@ -61,6 +63,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.Icon +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 @@ -123,9 +126,6 @@ fun TextComposer( is TextEditorState.Markdown -> state.state.text.value() is TextEditorState.Rich -> state.richTextEditorState.messageMarkdown } - val onSendClick = { - onSendMessage() - } val onPlayVoiceMessageClick = { onVoicePlayerEvent(VoiceMessagePlayerEvent.Play) @@ -238,20 +238,17 @@ fun TextComposer( val sendButton = @Composable { SendButton( canSendMessage = canSendMessage, - onClick = onSendClick, composerMode = composerMode, ) } val recordVoiceButton = @Composable { VoiceMessageRecorderButton( isRecording = voiceMessageState is VoiceMessageState.Recording, - onEvent = onVoiceRecorderEvent, ) } val sendVoiceButton = @Composable { SendButton( canSendMessage = voiceMessageState is VoiceMessageState.Preview, - onClick = onSendVoiceMessage, composerMode = composerMode, ) } @@ -265,6 +262,39 @@ fun TextComposer( @Composable { TextFormatting(state = it.richTextEditorState) } } + val hapticFeedback = LocalHapticFeedback.current + + fun performHapticFeedback() { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + } + + fun endButtonClickStandard() = when { + !canSendMessage -> + when (voiceMessageState) { + VoiceMessageState.Idle -> { + performHapticFeedback() + onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Start) + } + is VoiceMessageState.Recording -> { + performHapticFeedback() + onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Stop) + } + is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { + true -> { + // No op + } + false -> onSendVoiceMessage() + } + } + else -> onSendMessage() + } + + fun endButtonClickFormatting() { + if (canSendMessage) { + onSendMessage() + } + } + val sendOrRecordButton = when { !canSendMessage -> when (voiceMessageState) { @@ -330,8 +360,9 @@ fun TextComposer( ) }, textFormatting = textFormattingOptions, - endButtonA11y = endButtonA11y, sendButton = sendButton, + endButtonClick = ::endButtonClickFormatting, + endButtonA11y = endButtonA11y, ) } else { StandardLayout( @@ -341,6 +372,7 @@ fun TextComposer( composerOptionsButton = composerOptionsButton, textInput = textInput, endButton = sendOrRecordButton, + endButtonClick = ::endButtonClickStandard, endButtonA11y = endButtonA11y, voiceRecording = voiceRecording, voiceDeleteButton = voiceDeleteButton, @@ -409,6 +441,7 @@ private fun StandardLayout( voiceRecording: @Composable () -> Unit, voiceDeleteButton: @Composable () -> Unit, endButton: @Composable () -> Unit, + endButtonClick: () -> Unit, endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), modifier: Modifier = Modifier, ) { @@ -454,12 +487,13 @@ private fun StandardLayout( textInput() } } - Box( - Modifier + // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. + IconButton( + modifier = Modifier .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) .size(48.dp) .clearAndSetSemantics(endButtonA11y), - contentAlignment = Alignment.Center, + onClick = endButtonClick, ) { endButton() } @@ -496,6 +530,7 @@ private fun TextFormattingLayout( dismissTextFormattingButton: @Composable () -> Unit, textFormatting: @Composable () -> Unit, sendButton: @Composable () -> Unit, + endButtonClick: () -> Unit, endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), modifier: Modifier = Modifier ) { @@ -527,13 +562,16 @@ private fun TextFormattingLayout( Box(modifier = Modifier.weight(1f)) { textFormatting() } - Box( + // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. + IconButton( modifier = Modifier .padding( start = 14.dp, end = 6.dp, ) - .clearAndSetSemantics(endButtonA11y) + .size(48.dp) + .clearAndSetSemantics(endButtonA11y), + onClick = endButtonClick, ) { sendButton() } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index a136f63b09..e3ccd383f3 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -41,48 +41,40 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode @Composable internal fun SendButton( canSendMessage: Boolean, - onClick: () -> Unit, composerMode: MessageComposerMode, modifier: Modifier = Modifier, ) { - IconButton( + val iconVector = when { + composerMode.isEditing -> CompoundIcons.Check() + else -> CompoundIcons.SendSolid() + } + val iconStartPadding = when { + composerMode.isEditing -> 0.dp + else -> 2.dp + } + Box( modifier = modifier - .size(48.dp), - onClick = onClick, - enabled = canSendMessage, + .clip(CircleShape) + .size(36.dp) + .buttonBackgroundModifier(canSendMessage) ) { - val iconVector = when { - composerMode.isEditing -> CompoundIcons.Check() - else -> CompoundIcons.SendSolid() - } - val iconStartPadding = when { - composerMode.isEditing -> 0.dp - else -> 2.dp - } - Box( + Icon( modifier = Modifier - .clip(CircleShape) - .size(36.dp) - .buttonBackgroundModifier(canSendMessage) - ) { - Icon( - modifier = Modifier - .padding(start = iconStartPadding) - .align(Alignment.Center), - imageVector = iconVector, - // Note: accessibility is managed in TextComposer. - contentDescription = null, - tint = if (canSendMessage) { - if (ElementTheme.colors.isLight) { - ElementTheme.colors.iconOnSolidPrimary - } else { - ElementTheme.colors.iconPrimary - } + .padding(start = iconStartPadding) + .align(Alignment.Center), + imageVector = iconVector, + // Note: accessibility is managed in TextComposer. + contentDescription = null, + tint = if (canSendMessage) { + if (ElementTheme.colors.isLight) { + ElementTheme.colors.iconOnSolidPrimary } else { - ElementTheme.colors.iconQuaternary + ElementTheme.colors.iconPrimary } - ) - } + } else { + ElementTheme.colors.iconQuaternary + } + ) } } @@ -117,9 +109,17 @@ internal fun SendButtonPreview() = ElementPreview { val normalMode = MessageComposerMode.Normal val editMode = MessageComposerMode.Edit(EventId("\$id").toEventOrTransactionId(), "") Row { - SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode) - SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode) - SendButton(canSendMessage = true, onClick = {}, composerMode = editMode) - SendButton(canSendMessage = false, onClick = {}, composerMode = editMode) + IconButton(onClick = {}) { + SendButton(canSendMessage = true, composerMode = normalMode) + } + IconButton(onClick = {}) { + SendButton(canSendMessage = false, composerMode = normalMode) + } + IconButton(onClick = {}) { + SendButton(canSendMessage = true, composerMode = editMode) + } + IconButton(onClick = {}) { + SendButton(canSendMessage = false, composerMode = editMode) + } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt index 32fe2847c2..b512154375 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt @@ -14,9 +14,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons @@ -25,49 +24,25 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.utils.CommonDrawables -import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent @Composable internal fun VoiceMessageRecorderButton( isRecording: Boolean, - onEvent: (VoiceMessageRecorderEvent) -> Unit, modifier: Modifier = Modifier, ) { - val hapticFeedback = LocalHapticFeedback.current - - val performHapticFeedback = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - } - if (isRecording) { - StopButton( - modifier = modifier, - onClick = { - performHapticFeedback() - onEvent(VoiceMessageRecorderEvent.Stop) - } - ) + StopButton(modifier) } else { - StartButton( - modifier = modifier, - onClick = { - performHapticFeedback() - onEvent(VoiceMessageRecorderEvent.Start) - } - ) + StartButton(modifier) } } @Composable private fun StartButton( - onClick: () -> Unit, modifier: Modifier = Modifier, -) = IconButton( - modifier = modifier.size(48.dp), - onClick = onClick, ) { Icon( - modifier = Modifier.size(24.dp), + modifier = modifier.size(24.dp), imageVector = CompoundIcons.MicOn(), // Note: accessibility is managed in TextComposer. contentDescription = null, @@ -77,41 +52,40 @@ private fun StartButton( @Composable private fun StopButton( - onClick: () -> Unit, modifier: Modifier = Modifier, -) = IconButton( - modifier = modifier - .size(48.dp), - onClick = onClick, ) { Box( - Modifier + modifier .size(36.dp) .background( color = ElementTheme.colors.bgActionPrimaryRest, shape = CircleShape, - ) - ) - Icon( - modifier = Modifier.size(24.dp), - resourceId = CommonDrawables.ic_stop, - // Note: accessibility is managed in TextComposer. - contentDescription = null, - tint = ElementTheme.colors.iconOnSolidPrimary, - ) + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(24.dp), + resourceId = CommonDrawables.ic_stop, + // Note: accessibility is managed in TextComposer. + contentDescription = null, + tint = ElementTheme.colors.iconOnSolidPrimary, + ) + } } @PreviewsDayNight @Composable internal fun VoiceMessageRecorderButtonPreview() = ElementPreview { Row { - VoiceMessageRecorderButton( - isRecording = false, - onEvent = {}, - ) - VoiceMessageRecorderButton( - isRecording = true, - onEvent = {}, - ) + IconButton(onClick = {}) { + VoiceMessageRecorderButton( + isRecording = false, + ) + } + IconButton(onClick = {}) { + VoiceMessageRecorderButton( + isRecording = true, + ) + } } } From 83f72684247209ad6f7d447a09163348b7dbae43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jan 2026 11:35:52 +0100 Subject: [PATCH 3/8] Cleanup code. This if was not necessary. --- .../libraries/textcomposer/TextComposer.kt | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 200c6e63fb..2bc8e27320 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -453,17 +453,13 @@ private fun StandardLayout( } Row(verticalAlignment = Alignment.Bottom) { if (voiceMessageState !is VoiceMessageState.Idle) { - if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { - Box( - modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), - contentAlignment = Alignment.Center, - ) { - voiceDeleteButton() - } - } else { - Spacer(modifier = Modifier.width(16.dp)) + Box( + modifier = Modifier + .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .size(48.dp), + contentAlignment = Alignment.Center, + ) { + voiceDeleteButton() } Box( modifier = Modifier From 9f9a017ffa6f2cdf498819a7db0e0fb3c73ce8b0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jan 2026 11:42:05 +0100 Subject: [PATCH 4/8] Small rework to prepare a bugfix. No behavior / UI change. --- .../libraries/textcomposer/TextComposer.kt | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 2bc8e27320..b96a7e1462 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -452,7 +452,14 @@ private fun StandardLayout( Spacer(Modifier.height(4.dp)) } Row(verticalAlignment = Alignment.Bottom) { - if (voiceMessageState !is VoiceMessageState.Idle) { + if (voiceMessageState is VoiceMessageState.Idle) { + Box( + Modifier + .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) + ) { + composerOptionsButton() + } + } else { Box( modifier = Modifier .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) @@ -461,26 +468,16 @@ private fun StandardLayout( ) { voiceDeleteButton() } - Box( - modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) - ) { - voiceRecording() - } - } else { - Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) - ) { - composerOptionsButton() - } - Box( - modifier = Modifier - .padding(bottom = 8.dp, top = 8.dp) - .weight(1f) - ) { + } + Box( + modifier = Modifier + .padding(bottom = 8.dp, top = 8.dp) + .weight(1f) + ) { + if (voiceMessageState is VoiceMessageState.Idle) { textInput() + } else { + voiceRecording() } } // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. From 75fc734892fcdb7778f7473b9220eeab96bc66a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jan 2026 12:09:29 +0100 Subject: [PATCH 5/8] Ensure that the keyboard focus and accessibility focus is not lost when deleting a pending voice message. --- .../libraries/textcomposer/TextComposer.kt | 104 ++++++++++-------- .../components/VoiceMessageDeleteButton.kt | 46 ++++---- 2 files changed, 76 insertions(+), 74 deletions(-) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index b96a7e1462..4438715444 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn 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.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable @@ -143,26 +144,6 @@ fun TextComposer( .fillMaxSize() .height(IntrinsicSize.Min) - val composerOptionsButton: @Composable () -> Unit = remember(composerMode) { - @Composable { - when (composerMode) { - is MessageComposerMode.Attachment -> { - Spacer(modifier = Modifier.width(9.dp)) - } - is MessageComposerMode.EditCaption -> { - Spacer(modifier = Modifier.width(16.dp)) - } - else -> { - IconColorButton( - onClick = onAddAttachment, - imageVector = CompoundIcons.Plus(), - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - ) - } - } - } - } - val placeholder = if (composerMode.inThread) { stringResource(id = CommonStrings.action_reply_in_thread) } else if (composerMode is MessageComposerMode.Attachment || composerMode is MessageComposerMode.EditCaption) { @@ -337,16 +318,6 @@ fun TextComposer( } } - val voiceDeleteButton = @Composable { - when (voiceMessageState) { - is VoiceMessageState.Preview -> - VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending, onClick = onDeleteVoiceMessage) - is VoiceMessageState.Recording -> - VoiceMessageDeleteButton(enabled = true, onClick = { onVoiceRecorderEvent(VoiceMessageRecorderEvent.Cancel) }) - else -> {} - } - } - if (showTextFormatting && textFormattingOptions != null) { TextFormattingLayout( modifier = layoutModifier, @@ -366,16 +337,18 @@ fun TextComposer( ) } else { StandardLayout( + composerMode = composerMode, voiceMessageState = voiceMessageState, isRoomEncrypted = state.isRoomEncrypted, modifier = layoutModifier, - composerOptionsButton = composerOptionsButton, textInput = textInput, endButton = sendOrRecordButton, endButtonClick = ::endButtonClickStandard, endButtonA11y = endButtonA11y, voiceRecording = voiceRecording, - voiceDeleteButton = voiceDeleteButton, + onAddAttachment = onAddAttachment, + onDeleteVoiceMessage = onDeleteVoiceMessage, + onVoiceRecorderEvent = onVoiceRecorderEvent, ) } @@ -434,15 +407,17 @@ private fun endButtonA11y( @Composable private fun StandardLayout( + composerMode: MessageComposerMode, voiceMessageState: VoiceMessageState, isRoomEncrypted: Boolean?, textInput: @Composable () -> Unit, - composerOptionsButton: @Composable () -> Unit, voiceRecording: @Composable () -> Unit, - voiceDeleteButton: @Composable () -> Unit, endButton: @Composable () -> Unit, endButtonClick: () -> Unit, endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), + onAddAttachment: () -> Unit, + onDeleteVoiceMessage: () -> Unit, + onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -452,21 +427,54 @@ private fun StandardLayout( Spacer(Modifier.height(4.dp)) } Row(verticalAlignment = Alignment.Bottom) { - if (voiceMessageState is VoiceMessageState.Idle) { - Box( - Modifier - .padding(bottom = 5.dp, top = 5.dp, start = 3.dp) - ) { - composerOptionsButton() + when (composerMode) { + is MessageComposerMode.Attachment -> { + Spacer(modifier = Modifier.width(12.dp)) } - } else { - Box( - modifier = Modifier - .padding(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) - .size(48.dp), - contentAlignment = Alignment.Center, - ) { - voiceDeleteButton() + is MessageComposerMode.EditCaption -> { + Spacer(modifier = Modifier.width(19.dp)) + } + else -> { + val endPadding = if (voiceMessageState is VoiceMessageState.Idle) 0.dp else 3.dp + // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. + IconButton( + modifier = Modifier + .padding(top = 5.dp, bottom = 5.dp, start = 3.dp, end = endPadding) + .size(48.dp), + onClick = { + if (voiceMessageState is VoiceMessageState.Idle) { + onAddAttachment() + } else { + when (voiceMessageState) { + is VoiceMessageState.Preview -> if (!voiceMessageState.isSending) { + onDeleteVoiceMessage() + } + is VoiceMessageState.Recording -> + onVoiceRecorderEvent(VoiceMessageRecorderEvent.Cancel) + } + } + }, + ) { + if (voiceMessageState is VoiceMessageState.Idle) { + Icon( + modifier = Modifier + .clip(CircleShape) + .size(30.dp) + .background(ElementTheme.colors.iconPrimary) + .padding(3.dp), + imageVector = CompoundIcons.Plus(), + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), + tint = ElementTheme.colors.iconOnSolidPrimary + ) + } else { + when (voiceMessageState) { + is VoiceMessageState.Preview -> + VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending) + is VoiceMessageState.Recording -> + VoiceMessageDeleteButton(enabled = true) + } + } + } } } Box( diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt index af5f443cc7..3e72b309fe 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt @@ -25,39 +25,33 @@ import io.element.android.libraries.ui.strings.CommonStrings @Composable fun VoiceMessageDeleteButton( enabled: Boolean, - onClick: () -> Unit, modifier: Modifier = Modifier, ) { - IconButton( - modifier = modifier - .size(48.dp), - enabled = enabled, - onClick = onClick, - ) { - Icon( - modifier = Modifier.size(24.dp), - imageVector = CompoundIcons.Delete(), - contentDescription = stringResource(CommonStrings.a11y_delete), - tint = if (enabled) { - ElementTheme.colors.iconCriticalPrimary - } else { - ElementTheme.colors.iconDisabled - }, - ) - } + Icon( + modifier = modifier.size(24.dp), + imageVector = CompoundIcons.Delete(), + contentDescription = stringResource(CommonStrings.a11y_delete), + tint = if (enabled) { + ElementTheme.colors.iconCriticalPrimary + } else { + ElementTheme.colors.iconDisabled + }, + ) } @PreviewsDayNight @Composable internal fun VoiceMessageDeleteButtonPreview() = ElementPreview { Row { - VoiceMessageDeleteButton( - enabled = true, - onClick = {}, - ) - VoiceMessageDeleteButton( - enabled = false, - onClick = {}, - ) + IconButton(onClick = {}) { + VoiceMessageDeleteButton( + enabled = true, + ) + } + IconButton(onClick = {}) { + VoiceMessageDeleteButton( + enabled = false, + ) + } } } From d7eb302d497dcbadc3ef8ce43b3d084f7eb2d389 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 8 Jan 2026 12:44:36 +0000 Subject: [PATCH 6/8] Update screenshots --- ...es.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png | 4 ++-- ....textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png | 4 ++-- .../libraries.textcomposer_TextComposerVoice_Day_0_en.png | 4 ++-- .../libraries.textcomposer_TextComposerVoice_Night_0_en.png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png index 8301d09a79..82584525f2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09faeef5f5864f93d81f271a67826acf417b228987c6f918b95b31f91a468cb1 -size 36097 +oid sha256:74c638ff6c7ea4961af24b176c3f0b2b40ce123c5f058c4f1ba06c6b823cb16f +size 36090 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png index 943d0abc8e..5a65d27d21 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoiceNotEncrypted_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:246d4bf81c16bbbcfc82dd23b23bb1471e3d5e078bc7ecaba3c68d2ab3bb75c2 -size 34307 +oid sha256:8d220ce93b1e7a7a7330ee90f59415a07f49d040ad5c85ff8086b3dba882452d +size 34313 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png index 24be8a7dbc..39bffa708a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:274f54991bbd79c4220d945964caf7fc523ce99a70b748ef992cc15ee030fdf5 -size 25124 +oid sha256:75d285698529499a08b5a6107bed61de35b30a0e225951fc93edc1c632a5c7ba +size 25121 diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png index 433550b84b..e77b5da5e1 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerVoice_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:258643d514aeb7a53788ba2addc53fe46f888193ced2970c73887f9eb1584753 -size 24039 +oid sha256:77ac0f986e20bea03f10f00a699484c31a7959eba2ffb4764f8c1e71428159ed +size 24040 From b4f3cd29f93b68d92be528a4de77cc9fba027da3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jan 2026 10:27:39 +0100 Subject: [PATCH 7/8] Improve code readability. --- .../libraries/textcomposer/TextComposer.kt | 275 ++++++++++-------- .../{SendButton.kt => SendButtonIcon.kt} | 23 +- ...ton.kt => VoiceMessageDeleteButtonIcon.kt} | 8 +- ...n.kt => VoiceMessageRecorderButtonIcon.kt} | 8 +- 4 files changed, 169 insertions(+), 145 deletions(-) rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/{SendButton.kt => SendButtonIcon.kt} (80%) rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/{VoiceMessageDeleteButton.kt => VoiceMessageDeleteButtonIcon.kt} (89%) rename libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/{VoiceMessageRecorderButton.kt => VoiceMessageRecorderButtonIcon.kt} (92%) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index 4438715444..5f3e9ec54b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,7 +43,6 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.hideFromAccessibility @@ -74,11 +72,11 @@ 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.testtags.TestTags import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.textcomposer.components.SendButton +import io.element.android.libraries.textcomposer.components.SendButtonIcon import io.element.android.libraries.textcomposer.components.TextFormatting -import io.element.android.libraries.textcomposer.components.VoiceMessageDeleteButton +import io.element.android.libraries.textcomposer.components.VoiceMessageDeleteButtonIcon import io.element.android.libraries.textcomposer.components.VoiceMessagePreview -import io.element.android.libraries.textcomposer.components.VoiceMessageRecorderButton +import io.element.android.libraries.textcomposer.components.VoiceMessageRecorderButtonIcon import io.element.android.libraries.textcomposer.components.VoiceMessageRecording import io.element.android.libraries.textcomposer.components.markdown.MarkdownTextInput import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape @@ -215,29 +213,7 @@ fun TextComposer( } } - val canSendMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment - val sendButton = @Composable { - SendButton( - canSendMessage = canSendMessage, - composerMode = composerMode, - ) - } - val recordVoiceButton = @Composable { - VoiceMessageRecorderButton( - isRecording = voiceMessageState is VoiceMessageState.Recording, - ) - } - val sendVoiceButton = @Composable { - SendButton( - canSendMessage = voiceMessageState is VoiceMessageState.Preview, - composerMode = composerMode, - ) - } - val uploadVoiceProgress = @Composable { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - ) - } + val canSendTextMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment val textFormattingOptions: @Composable (() -> Unit)? = (state as? TextEditorState.Rich)?.let { @Composable { TextFormatting(state = it.richTextEditorState) } @@ -249,52 +225,126 @@ fun TextComposer( hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) } - fun endButtonClickStandard() = when { - !canSendMessage -> - when (voiceMessageState) { - VoiceMessageState.Idle -> { - performHapticFeedback() - onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Start) - } - is VoiceMessageState.Recording -> { - performHapticFeedback() - onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Stop) - } - is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { - true -> { - // No op + @Composable + fun rememberEndButtonParams() = remember( + composerMode.isEditing, + voiceMessageState.endButtonKey(), + canSendTextMessage, + ) { + when { + !canSendTextMessage -> + when (voiceMessageState) { + VoiceMessageState.Idle -> EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.a11y_voice_message_record, + endButtonClick = { + performHapticFeedback() + onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Start) + }, + endButtonContent = @Composable { + VoiceMessageRecorderButtonIcon( + isRecording = false, + ) + } + ) + is VoiceMessageState.Recording -> EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.a11y_voice_message_stop_recording, + endButtonClick = { + performHapticFeedback() + onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Stop) + }, + endButtonContent = @Composable { + VoiceMessageRecorderButtonIcon( + isRecording = true, + ) + } + ) + is VoiceMessageState.Preview -> if (voiceMessageState.isSending) { + EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.common_sending, + endButtonClick = {}, + endButtonContent = @Composable { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + ) + } + ) + } else { + EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.action_send_voice_message, + endButtonClick = { + onSendVoiceMessage() + }, + endButtonContent = @Composable { + SendButtonIcon( + canSendMessage = true, + isEditing = composerMode.isEditing, + ) + }, + ) } - false -> onSendVoiceMessage() } - } - else -> onSendMessage() - } - - fun endButtonClickFormatting() { - if (canSendMessage) { - onSendMessage() + composerMode.isEditing -> EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.action_send_edited_message, + endButtonClick = { + onSendMessage() + }, + endButtonContent = @Composable { + SendButtonIcon( + canSendMessage = true, + isEditing = true, + ) + }, + ) + else -> EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.action_send_message, + endButtonClick = { + onSendMessage() + }, + endButtonContent = @Composable { + SendButtonIcon( + canSendMessage = true, + isEditing = false, + ) + }, + ) } } - val sendOrRecordButton = when { - !canSendMessage -> - when (voiceMessageState) { - VoiceMessageState.Idle, - is VoiceMessageState.Recording -> recordVoiceButton - is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { - true -> uploadVoiceProgress - false -> sendVoiceButton - } - } - else -> sendButton + @Composable + fun rememberEndButtonParamsFormatting() = remember(composerMode.isEditing, canSendTextMessage) { + if (composerMode.isEditing) { + EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.action_send_edited_message, + endButtonClick = { + if (canSendTextMessage) { + onSendMessage() + } + }, + endButtonContent = @Composable { + SendButtonIcon( + canSendMessage = canSendTextMessage, + isEditing = true, + ) + }, + ) + } else { + EndButtonParams( + endButtonContentDescriptionResId = CommonStrings.action_send_message, + endButtonClick = { + if (canSendTextMessage) { + onSendMessage() + } + }, + endButtonContent = @Composable { + SendButtonIcon( + canSendMessage = canSendTextMessage, + isEditing = false, + ) + }, + ) + } } - val endButtonA11y = endButtonA11y( - composerMode = composerMode, - voiceMessageState = voiceMessageState, - canSendMessage = canSendMessage, - ) - val voiceRecording = @Composable { when (voiceMessageState) { is VoiceMessageState.Preview -> @@ -319,6 +369,7 @@ fun TextComposer( } if (showTextFormatting && textFormattingOptions != null) { + val endButtonParams = rememberEndButtonParamsFormatting() TextFormattingLayout( modifier = layoutModifier, isRoomEncrypted = state.isRoomEncrypted, @@ -331,20 +382,17 @@ fun TextComposer( ) }, textFormatting = textFormattingOptions, - sendButton = sendButton, - endButtonClick = ::endButtonClickFormatting, - endButtonA11y = endButtonA11y, + endButtonParams = endButtonParams, ) } else { + val endButtonParams = rememberEndButtonParams() StandardLayout( composerMode = composerMode, voiceMessageState = voiceMessageState, isRoomEncrypted = state.isRoomEncrypted, modifier = layoutModifier, textInput = textInput, - endButton = sendOrRecordButton, - endButtonClick = ::endButtonClickStandard, - endButtonA11y = endButtonA11y, + endButtonParams = endButtonParams, voiceRecording = voiceRecording, onAddAttachment = onAddAttachment, onDeleteVoiceMessage = onDeleteVoiceMessage, @@ -372,38 +420,11 @@ fun TextComposer( } } -@ReadOnlyComposable -@Composable -private fun endButtonA11y( - composerMode: MessageComposerMode, - voiceMessageState: VoiceMessageState, - canSendMessage: Boolean, -): (SemanticsPropertyReceiver) -> Unit { - val a11ySendButtonDescription = stringResource( - id = when { - !canSendMessage -> - when (voiceMessageState) { - VoiceMessageState.Idle, - is VoiceMessageState.Recording -> if (voiceMessageState is VoiceMessageState.Recording) { - CommonStrings.a11y_voice_message_stop_recording - } else { - CommonStrings.a11y_voice_message_record - } - is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { - true -> CommonStrings.common_sending - false -> CommonStrings.action_send_voice_message - } - } - composerMode.isEditing -> CommonStrings.action_send_edited_message - else -> CommonStrings.action_send_message - } - ) - val endButtonA11y: (SemanticsPropertyReceiver.() -> Unit) = { - contentDescription = a11ySendButtonDescription - onClick(null, null) - } - return endButtonA11y -} +private data class EndButtonParams( + val endButtonContentDescriptionResId: Int, + val endButtonClick: () -> Unit, + val endButtonContent: @Composable () -> Unit, +) @Composable private fun StandardLayout( @@ -412,9 +433,7 @@ private fun StandardLayout( isRoomEncrypted: Boolean?, textInput: @Composable () -> Unit, voiceRecording: @Composable () -> Unit, - endButton: @Composable () -> Unit, - endButtonClick: () -> Unit, - endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), + endButtonParams: EndButtonParams, onAddAttachment: () -> Unit, onDeleteVoiceMessage: () -> Unit, onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit, @@ -469,9 +488,9 @@ private fun StandardLayout( } else { when (voiceMessageState) { is VoiceMessageState.Preview -> - VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending) + VoiceMessageDeleteButtonIcon(enabled = !voiceMessageState.isSending) is VoiceMessageState.Recording -> - VoiceMessageDeleteButton(enabled = true) + VoiceMessageDeleteButtonIcon(enabled = true) } } } @@ -489,15 +508,18 @@ private fun StandardLayout( } } // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. + val endButtonContentDescription = stringResource(endButtonParams.endButtonContentDescriptionResId) IconButton( modifier = Modifier .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) .size(48.dp) - .clearAndSetSemantics(endButtonA11y), - onClick = endButtonClick, - ) { - endButton() - } + .clearAndSetSemantics { + contentDescription = endButtonContentDescription + onClick(null, null) + }, + onClick = endButtonParams.endButtonClick, + content = endButtonParams.endButtonContent, + ) } } } @@ -530,9 +552,7 @@ private fun TextFormattingLayout( textInput: @Composable () -> Unit, dismissTextFormattingButton: @Composable () -> Unit, textFormatting: @Composable () -> Unit, - sendButton: @Composable () -> Unit, - endButtonClick: () -> Unit, - endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), + endButtonParams: EndButtonParams, modifier: Modifier = Modifier ) { Column( @@ -564,6 +584,7 @@ private fun TextFormattingLayout( textFormatting() } // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. + val endButtonContentDescription = stringResource(endButtonParams.endButtonContentDescriptionResId) IconButton( modifier = Modifier .padding( @@ -571,11 +592,13 @@ private fun TextFormattingLayout( end = 6.dp, ) .size(48.dp) - .clearAndSetSemantics(endButtonA11y), - onClick = endButtonClick, - ) { - sendButton() - } + .clearAndSetSemantics { + contentDescription = endButtonContentDescription + onClick(null, null) + }, + onClick = endButtonParams.endButtonClick, + content = endButtonParams.endButtonContent, + ) } } } @@ -635,6 +658,12 @@ private fun TextInputBox( } } +private fun VoiceMessageState.endButtonKey() = when (this) { + is VoiceMessageState.Idle -> "Idle" + is VoiceMessageState.Preview -> "Preview_$isSending" + is VoiceMessageState.Recording -> "Recording" +} + private fun aTextEditorStateMarkdownList(isRoomEncrypted: Boolean? = null) = persistentListOf( aTextEditorStateMarkdown(initialText = "", initialFocus = true, isRoomEncrypted = isRoomEncrypted), aTextEditorStateMarkdown(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted), diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButtonIcon.kt similarity index 80% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButtonIcon.kt index e3ccd383f3..d2d11c321b 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButtonIcon.kt @@ -29,9 +29,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId -import io.element.android.libraries.textcomposer.model.MessageComposerMode /** * Send button for the message composer. @@ -39,17 +36,17 @@ import io.element.android.libraries.textcomposer.model.MessageComposerMode * Temporary Figma : https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?node-id=2274-39944&m=dev */ @Composable -internal fun SendButton( +internal fun SendButtonIcon( canSendMessage: Boolean, - composerMode: MessageComposerMode, + isEditing: Boolean, modifier: Modifier = Modifier, ) { val iconVector = when { - composerMode.isEditing -> CompoundIcons.Check() + isEditing -> CompoundIcons.Check() else -> CompoundIcons.SendSolid() } val iconStartPadding = when { - composerMode.isEditing -> 0.dp + isEditing -> 0.dp else -> 2.dp } Box( @@ -105,21 +102,19 @@ private fun Modifier.buttonBackgroundModifier( @PreviewsDayNight @Composable -internal fun SendButtonPreview() = ElementPreview { - val normalMode = MessageComposerMode.Normal - val editMode = MessageComposerMode.Edit(EventId("\$id").toEventOrTransactionId(), "") +internal fun SendButtonIconPreview() = ElementPreview { Row { IconButton(onClick = {}) { - SendButton(canSendMessage = true, composerMode = normalMode) + SendButtonIcon(canSendMessage = true, isEditing = false) } IconButton(onClick = {}) { - SendButton(canSendMessage = false, composerMode = normalMode) + SendButtonIcon(canSendMessage = false, isEditing = false) } IconButton(onClick = {}) { - SendButton(canSendMessage = true, composerMode = editMode) + SendButtonIcon(canSendMessage = true, isEditing = true) } IconButton(onClick = {}) { - SendButton(canSendMessage = false, composerMode = editMode) + SendButtonIcon(canSendMessage = false, isEditing = true) } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButtonIcon.kt similarity index 89% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButtonIcon.kt index 3e72b309fe..182a5d5a52 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageDeleteButtonIcon.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.ui.strings.CommonStrings @Composable -fun VoiceMessageDeleteButton( +fun VoiceMessageDeleteButtonIcon( enabled: Boolean, modifier: Modifier = Modifier, ) { @@ -41,15 +41,15 @@ fun VoiceMessageDeleteButton( @PreviewsDayNight @Composable -internal fun VoiceMessageDeleteButtonPreview() = ElementPreview { +internal fun VoiceMessageDeleteButtonIconPreview() = ElementPreview { Row { IconButton(onClick = {}) { - VoiceMessageDeleteButton( + VoiceMessageDeleteButtonIcon( enabled = true, ) } IconButton(onClick = {}) { - VoiceMessageDeleteButton( + VoiceMessageDeleteButtonIcon( enabled = false, ) } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButtonIcon.kt similarity index 92% rename from libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt rename to libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButtonIcon.kt index b512154375..aeeaf839c3 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/VoiceMessageRecorderButtonIcon.kt @@ -26,7 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.utils.CommonDrawables @Composable -internal fun VoiceMessageRecorderButton( +internal fun VoiceMessageRecorderButtonIcon( isRecording: Boolean, modifier: Modifier = Modifier, ) { @@ -75,15 +75,15 @@ private fun StopButton( @PreviewsDayNight @Composable -internal fun VoiceMessageRecorderButtonPreview() = ElementPreview { +internal fun VoiceMessageRecorderButtonIconPreview() = ElementPreview { Row { IconButton(onClick = {}) { - VoiceMessageRecorderButton( + VoiceMessageRecorderButtonIcon( isRecording = false, ) } IconButton(onClick = {}) { - VoiceMessageRecorderButton( + VoiceMessageRecorderButtonIcon( isRecording = true, ) } From 1016363dd977fc6b5e78428d53412b839a1c936f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 12 Jan 2026 10:36:41 +0000 Subject: [PATCH 8/8] Update screenshots --- ...libraries.textcomposer.components_SendButtonIcon_Day_0_en.png} | 0 ...braries.textcomposer.components_SendButtonIcon_Night_0_en.png} | 0 ...composer.components_VoiceMessageDeleteButtonIcon_Day_0_en.png} | 0 ...mposer.components_VoiceMessageDeleteButtonIcon_Night_0_en.png} | 0 ...mposer.components_VoiceMessageRecorderButtonIcon_Day_0_en.png} | 0 ...oser.components_VoiceMessageRecorderButtonIcon_Night_0_en.png} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename tests/uitests/src/test/snapshots/images/{libraries.textcomposer.components_SendButton_Day_0_en.png => libraries.textcomposer.components_SendButtonIcon_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{libraries.textcomposer.components_SendButton_Night_0_en.png => libraries.textcomposer.components_SendButtonIcon_Night_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png => libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png => libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Night_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{libraries.textcomposer.components_VoiceMessageRecorderButton_Day_0_en.png => libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Day_0_en.png} (100%) rename tests/uitests/src/test/snapshots/images/{libraries.textcomposer.components_VoiceMessageRecorderButton_Night_0_en.png => libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Night_0_en.png} (100%) diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButtonIcon_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButtonIcon_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButtonIcon_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButton_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_SendButtonIcon_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButton_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageDeleteButtonIcon_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Day_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButton_Day_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Day_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Night_0_en.png similarity index 100% rename from tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButton_Night_0_en.png rename to tests/uitests/src/test/snapshots/images/libraries.textcomposer.components_VoiceMessageRecorderButtonIcon_Night_0_en.png