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, ) }