Improve code readability.

This commit is contained in:
Benoit Marty 2026-01-12 10:27:39 +01:00
parent d7eb302d49
commit b4f3cd29f9
4 changed files with 169 additions and 145 deletions

View file

@ -30,7 +30,6 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.LocalHapticFeedback
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.hideFromAccessibility 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.matrix.ui.messages.reply.InReplyToDetailsProvider
import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.testtags.testTag 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.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.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.VoiceMessageRecording
import io.element.android.libraries.textcomposer.components.markdown.MarkdownTextInput import io.element.android.libraries.textcomposer.components.markdown.MarkdownTextInput
import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape
@ -215,29 +213,7 @@ fun TextComposer(
} }
} }
val canSendMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment val canSendTextMessage = 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 textFormattingOptions: @Composable (() -> Unit)? = (state as? TextEditorState.Rich)?.let { val textFormattingOptions: @Composable (() -> Unit)? = (state as? TextEditorState.Rich)?.let {
@Composable { TextFormatting(state = it.richTextEditorState) } @Composable { TextFormatting(state = it.richTextEditorState) }
@ -249,52 +225,126 @@ fun TextComposer(
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
} }
fun endButtonClickStandard() = when { @Composable
!canSendMessage -> fun rememberEndButtonParams() = remember(
when (voiceMessageState) { composerMode.isEditing,
VoiceMessageState.Idle -> { voiceMessageState.endButtonKey(),
performHapticFeedback() canSendTextMessage,
onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Start) ) {
} when {
is VoiceMessageState.Recording -> { !canSendTextMessage ->
performHapticFeedback() when (voiceMessageState) {
onVoiceRecorderEvent.invoke(VoiceMessageRecorderEvent.Stop) VoiceMessageState.Idle -> EndButtonParams(
} endButtonContentDescriptionResId = CommonStrings.a11y_voice_message_record,
is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { endButtonClick = {
true -> { performHapticFeedback()
// No op 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()
} }
} composerMode.isEditing -> EndButtonParams(
else -> onSendMessage() endButtonContentDescriptionResId = CommonStrings.action_send_edited_message,
} endButtonClick = {
onSendMessage()
fun endButtonClickFormatting() { },
if (canSendMessage) { endButtonContent = @Composable {
onSendMessage() SendButtonIcon(
canSendMessage = true,
isEditing = true,
)
},
)
else -> EndButtonParams(
endButtonContentDescriptionResId = CommonStrings.action_send_message,
endButtonClick = {
onSendMessage()
},
endButtonContent = @Composable {
SendButtonIcon(
canSendMessage = true,
isEditing = false,
)
},
)
} }
} }
val sendOrRecordButton = when { @Composable
!canSendMessage -> fun rememberEndButtonParamsFormatting() = remember(composerMode.isEditing, canSendTextMessage) {
when (voiceMessageState) { if (composerMode.isEditing) {
VoiceMessageState.Idle, EndButtonParams(
is VoiceMessageState.Recording -> recordVoiceButton endButtonContentDescriptionResId = CommonStrings.action_send_edited_message,
is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { endButtonClick = {
true -> uploadVoiceProgress if (canSendTextMessage) {
false -> sendVoiceButton onSendMessage()
} }
} },
else -> sendButton 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 { val voiceRecording = @Composable {
when (voiceMessageState) { when (voiceMessageState) {
is VoiceMessageState.Preview -> is VoiceMessageState.Preview ->
@ -319,6 +369,7 @@ fun TextComposer(
} }
if (showTextFormatting && textFormattingOptions != null) { if (showTextFormatting && textFormattingOptions != null) {
val endButtonParams = rememberEndButtonParamsFormatting()
TextFormattingLayout( TextFormattingLayout(
modifier = layoutModifier, modifier = layoutModifier,
isRoomEncrypted = state.isRoomEncrypted, isRoomEncrypted = state.isRoomEncrypted,
@ -331,20 +382,17 @@ fun TextComposer(
) )
}, },
textFormatting = textFormattingOptions, textFormatting = textFormattingOptions,
sendButton = sendButton, endButtonParams = endButtonParams,
endButtonClick = ::endButtonClickFormatting,
endButtonA11y = endButtonA11y,
) )
} else { } else {
val endButtonParams = rememberEndButtonParams()
StandardLayout( StandardLayout(
composerMode = composerMode, composerMode = composerMode,
voiceMessageState = voiceMessageState, voiceMessageState = voiceMessageState,
isRoomEncrypted = state.isRoomEncrypted, isRoomEncrypted = state.isRoomEncrypted,
modifier = layoutModifier, modifier = layoutModifier,
textInput = textInput, textInput = textInput,
endButton = sendOrRecordButton, endButtonParams = endButtonParams,
endButtonClick = ::endButtonClickStandard,
endButtonA11y = endButtonA11y,
voiceRecording = voiceRecording, voiceRecording = voiceRecording,
onAddAttachment = onAddAttachment, onAddAttachment = onAddAttachment,
onDeleteVoiceMessage = onDeleteVoiceMessage, onDeleteVoiceMessage = onDeleteVoiceMessage,
@ -372,38 +420,11 @@ fun TextComposer(
} }
} }
@ReadOnlyComposable private data class EndButtonParams(
@Composable val endButtonContentDescriptionResId: Int,
private fun endButtonA11y( val endButtonClick: () -> Unit,
composerMode: MessageComposerMode, val endButtonContent: @Composable () -> Unit,
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
}
@Composable @Composable
private fun StandardLayout( private fun StandardLayout(
@ -412,9 +433,7 @@ private fun StandardLayout(
isRoomEncrypted: Boolean?, isRoomEncrypted: Boolean?,
textInput: @Composable () -> Unit, textInput: @Composable () -> Unit,
voiceRecording: @Composable () -> Unit, voiceRecording: @Composable () -> Unit,
endButton: @Composable () -> Unit, endButtonParams: EndButtonParams,
endButtonClick: () -> Unit,
endButtonA11y: (SemanticsPropertyReceiver.() -> Unit),
onAddAttachment: () -> Unit, onAddAttachment: () -> Unit,
onDeleteVoiceMessage: () -> Unit, onDeleteVoiceMessage: () -> Unit,
onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit, onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit,
@ -469,9 +488,9 @@ private fun StandardLayout(
} else { } else {
when (voiceMessageState) { when (voiceMessageState) {
is VoiceMessageState.Preview -> is VoiceMessageState.Preview ->
VoiceMessageDeleteButton(enabled = !voiceMessageState.isSending) VoiceMessageDeleteButtonIcon(enabled = !voiceMessageState.isSending)
is VoiceMessageState.Recording -> 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. // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled.
val endButtonContentDescription = stringResource(endButtonParams.endButtonContentDescriptionResId)
IconButton( IconButton(
modifier = Modifier modifier = Modifier
.padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp)
.size(48.dp) .size(48.dp)
.clearAndSetSemantics(endButtonA11y), .clearAndSetSemantics {
onClick = endButtonClick, contentDescription = endButtonContentDescription
) { onClick(null, null)
endButton() },
} onClick = endButtonParams.endButtonClick,
content = endButtonParams.endButtonContent,
)
} }
} }
} }
@ -530,9 +552,7 @@ private fun TextFormattingLayout(
textInput: @Composable () -> Unit, textInput: @Composable () -> Unit,
dismissTextFormattingButton: @Composable () -> Unit, dismissTextFormattingButton: @Composable () -> Unit,
textFormatting: @Composable () -> Unit, textFormatting: @Composable () -> Unit,
sendButton: @Composable () -> Unit, endButtonParams: EndButtonParams,
endButtonClick: () -> Unit,
endButtonA11y: (SemanticsPropertyReceiver.() -> Unit),
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Column( Column(
@ -564,6 +584,7 @@ private fun TextFormattingLayout(
textFormatting() textFormatting()
} }
// To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled. // To avoid loosing keyboard focus, the IconButton has to be defined here and has to be always enabled.
val endButtonContentDescription = stringResource(endButtonParams.endButtonContentDescriptionResId)
IconButton( IconButton(
modifier = Modifier modifier = Modifier
.padding( .padding(
@ -571,11 +592,13 @@ private fun TextFormattingLayout(
end = 6.dp, end = 6.dp,
) )
.size(48.dp) .size(48.dp)
.clearAndSetSemantics(endButtonA11y), .clearAndSetSemantics {
onClick = endButtonClick, contentDescription = endButtonContentDescription
) { onClick(null, null)
sendButton() },
} 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( private fun aTextEditorStateMarkdownList(isRoomEncrypted: Boolean? = null) = persistentListOf(
aTextEditorStateMarkdown(initialText = "", initialFocus = true, isRoomEncrypted = isRoomEncrypted), aTextEditorStateMarkdown(initialText = "", initialFocus = true, isRoomEncrypted = isRoomEncrypted),
aTextEditorStateMarkdown(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted), aTextEditorStateMarkdown(initialText = "A message", initialFocus = true, isRoomEncrypted = isRoomEncrypted),

View file

@ -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.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Icon 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.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. * 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 * Temporary Figma : https://www.figma.com/design/Ni6Ii8YKtmXCKYNE90cC67/Timeline-(new)?node-id=2274-39944&m=dev
*/ */
@Composable @Composable
internal fun SendButton( internal fun SendButtonIcon(
canSendMessage: Boolean, canSendMessage: Boolean,
composerMode: MessageComposerMode, isEditing: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val iconVector = when { val iconVector = when {
composerMode.isEditing -> CompoundIcons.Check() isEditing -> CompoundIcons.Check()
else -> CompoundIcons.SendSolid() else -> CompoundIcons.SendSolid()
} }
val iconStartPadding = when { val iconStartPadding = when {
composerMode.isEditing -> 0.dp isEditing -> 0.dp
else -> 2.dp else -> 2.dp
} }
Box( Box(
@ -105,21 +102,19 @@ private fun Modifier.buttonBackgroundModifier(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun SendButtonPreview() = ElementPreview { internal fun SendButtonIconPreview() = ElementPreview {
val normalMode = MessageComposerMode.Normal
val editMode = MessageComposerMode.Edit(EventId("\$id").toEventOrTransactionId(), "")
Row { Row {
IconButton(onClick = {}) { IconButton(onClick = {}) {
SendButton(canSendMessage = true, composerMode = normalMode) SendButtonIcon(canSendMessage = true, isEditing = false)
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
SendButton(canSendMessage = false, composerMode = normalMode) SendButtonIcon(canSendMessage = false, isEditing = false)
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
SendButton(canSendMessage = true, composerMode = editMode) SendButtonIcon(canSendMessage = true, isEditing = true)
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
SendButton(canSendMessage = false, composerMode = editMode) SendButtonIcon(canSendMessage = false, isEditing = true)
} }
} }
} }

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun VoiceMessageDeleteButton( fun VoiceMessageDeleteButtonIcon(
enabled: Boolean, enabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -41,15 +41,15 @@ fun VoiceMessageDeleteButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun VoiceMessageDeleteButtonPreview() = ElementPreview { internal fun VoiceMessageDeleteButtonIconPreview() = ElementPreview {
Row { Row {
IconButton(onClick = {}) { IconButton(onClick = {}) {
VoiceMessageDeleteButton( VoiceMessageDeleteButtonIcon(
enabled = true, enabled = true,
) )
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
VoiceMessageDeleteButton( VoiceMessageDeleteButtonIcon(
enabled = false, enabled = false,
) )
} }

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.utils.CommonDrawables import io.element.android.libraries.designsystem.utils.CommonDrawables
@Composable @Composable
internal fun VoiceMessageRecorderButton( internal fun VoiceMessageRecorderButtonIcon(
isRecording: Boolean, isRecording: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -75,15 +75,15 @@ private fun StopButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun VoiceMessageRecorderButtonPreview() = ElementPreview { internal fun VoiceMessageRecorderButtonIconPreview() = ElementPreview {
Row { Row {
IconButton(onClick = {}) { IconButton(onClick = {}) {
VoiceMessageRecorderButton( VoiceMessageRecorderButtonIcon(
isRecording = false, isRecording = false,
) )
} }
IconButton(onClick = {}) { IconButton(onClick = {}) {
VoiceMessageRecorderButton( VoiceMessageRecorderButtonIcon(
isRecording = true, isRecording = true,
) )
} }