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..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 @@ -27,9 +27,9 @@ 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 import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,9 +39,10 @@ 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 import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.hideFromAccessibility @@ -61,6 +62,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 @@ -70,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 @@ -123,9 +125,6 @@ fun TextComposer( is TextEditorState.Markdown -> state.state.text.value() is TextEditorState.Rich -> state.richTextEditorState.messageMarkdown } - val onSendClick = { - onSendMessage() - } val onPlayVoiceMessageClick = { onVoicePlayerEvent(VoiceMessagePlayerEvent.Play) @@ -143,26 +142,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) { @@ -234,55 +213,137 @@ fun TextComposer( } } - val canSendMessage = markdown.isNotBlank() || composerMode is MessageComposerMode.Attachment - 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, - ) - } - 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) } } - val sendOrRecordButton = when { - !canSendMessage -> - when (voiceMessageState) { - VoiceMessageState.Idle, - is VoiceMessageState.Recording -> recordVoiceButton - is VoiceMessageState.Preview -> when (voiceMessageState.isSending) { - true -> uploadVoiceProgress - false -> sendVoiceButton - } - } - else -> sendButton + val hapticFeedback = LocalHapticFeedback.current + + fun performHapticFeedback() { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) } - val endButtonA11y = endButtonA11y( - composerMode = composerMode, - voiceMessageState = voiceMessageState, - canSendMessage = canSendMessage, - ) + @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, + ) + }, + ) + } + } + 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, + ) + }, + ) + } + } + + @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 voiceRecording = @Composable { when (voiceMessageState) { @@ -307,17 +368,8 @@ 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) { + val endButtonParams = rememberEndButtonParamsFormatting() TextFormattingLayout( modifier = layoutModifier, isRoomEncrypted = state.isRoomEncrypted, @@ -330,20 +382,21 @@ fun TextComposer( ) }, textFormatting = textFormattingOptions, - endButtonA11y = endButtonA11y, - sendButton = sendButton, + endButtonParams = endButtonParams, ) } else { + val endButtonParams = rememberEndButtonParams() StandardLayout( + composerMode = composerMode, voiceMessageState = voiceMessageState, isRoomEncrypted = state.isRoomEncrypted, modifier = layoutModifier, - composerOptionsButton = composerOptionsButton, textInput = textInput, - endButton = sendOrRecordButton, - endButtonA11y = endButtonA11y, + endButtonParams = endButtonParams, voiceRecording = voiceRecording, - voiceDeleteButton = voiceDeleteButton, + onAddAttachment = onAddAttachment, + onDeleteVoiceMessage = onDeleteVoiceMessage, + onVoiceRecorderEvent = onVoiceRecorderEvent, ) } @@ -367,49 +420,23 @@ 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( + composerMode: MessageComposerMode, voiceMessageState: VoiceMessageState, isRoomEncrypted: Boolean?, textInput: @Composable () -> Unit, - composerOptionsButton: @Composable () -> Unit, voiceRecording: @Composable () -> Unit, - voiceDeleteButton: @Composable () -> Unit, - endButton: @Composable () -> Unit, - endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), + endButtonParams: EndButtonParams, + onAddAttachment: () -> Unit, + onDeleteVoiceMessage: () -> Unit, + onVoiceRecorderEvent: (VoiceMessageRecorderEvent) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { @@ -419,50 +446,80 @@ private fun StandardLayout( Spacer(Modifier.height(4.dp)) } Row(verticalAlignment = Alignment.Bottom) { - if (voiceMessageState !is VoiceMessageState.Idle) { - if (voiceMessageState is VoiceMessageState.Preview || voiceMessageState is VoiceMessageState.Recording) { - Box( + when (composerMode) { + is MessageComposerMode.Attachment -> { + Spacer(modifier = Modifier.width(12.dp)) + } + 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(bottom = 5.dp, top = 5.dp, end = 3.dp, start = 3.dp) + .padding(top = 5.dp, bottom = 5.dp, start = 3.dp, end = endPadding) .size(48.dp), - contentAlignment = Alignment.Center, + onClick = { + if (voiceMessageState is VoiceMessageState.Idle) { + onAddAttachment() + } else { + when (voiceMessageState) { + is VoiceMessageState.Preview -> if (!voiceMessageState.isSending) { + onDeleteVoiceMessage() + } + is VoiceMessageState.Recording -> + onVoiceRecorderEvent(VoiceMessageRecorderEvent.Cancel) + } + } + }, ) { - voiceDeleteButton() + 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 -> + VoiceMessageDeleteButtonIcon(enabled = !voiceMessageState.isSending) + is VoiceMessageState.Recording -> + VoiceMessageDeleteButtonIcon(enabled = true) + } + } } - } else { - Spacer(modifier = Modifier.width(16.dp)) - } - 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) - ) { - textInput() } } Box( - Modifier + 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. + 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), - contentAlignment = Alignment.Center, - ) { - endButton() - } + .clearAndSetSemantics { + contentDescription = endButtonContentDescription + onClick(null, null) + }, + onClick = endButtonParams.endButtonClick, + content = endButtonParams.endButtonContent, + ) } } } @@ -495,8 +552,7 @@ private fun TextFormattingLayout( textInput: @Composable () -> Unit, dismissTextFormattingButton: @Composable () -> Unit, textFormatting: @Composable () -> Unit, - sendButton: @Composable () -> Unit, - endButtonA11y: (SemanticsPropertyReceiver.() -> Unit), + endButtonParams: EndButtonParams, modifier: Modifier = Modifier ) { Column( @@ -527,16 +583,22 @@ 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. + val endButtonContentDescription = stringResource(endButtonParams.endButtonContentDescriptionResId) + IconButton( modifier = Modifier .padding( start = 14.dp, end = 6.dp, ) - .clearAndSetSemantics(endButtonA11y) - ) { - sendButton() - } + .size(48.dp) + .clearAndSetSemantics { + contentDescription = endButtonContentDescription + onClick(null, null) + }, + onClick = endButtonParams.endButtonClick, + content = endButtonParams.endButtonContent, + ) } } } @@ -596,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 57% 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 a136f63b09..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,50 +36,42 @@ 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, - onClick: () -> Unit, - composerMode: MessageComposerMode, + isEditing: Boolean, modifier: Modifier = Modifier, ) { - IconButton( + val iconVector = when { + isEditing -> CompoundIcons.Check() + else -> CompoundIcons.SendSolid() + } + val iconStartPadding = when { + 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 + } + ) } } @@ -113,13 +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 { - 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 = {}) { + SendButtonIcon(canSendMessage = true, isEditing = false) + } + IconButton(onClick = {}) { + SendButtonIcon(canSendMessage = false, isEditing = false) + } + IconButton(onClick = {}) { + SendButtonIcon(canSendMessage = true, isEditing = true) + } + IconButton(onClick = {}) { + 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 58% 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 af5f443cc7..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,41 +23,35 @@ import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.ui.strings.CommonStrings @Composable -fun VoiceMessageDeleteButton( +fun VoiceMessageDeleteButtonIcon( 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 { +internal fun VoiceMessageDeleteButtonIconPreview() = ElementPreview { Row { - VoiceMessageDeleteButton( - enabled = true, - onClick = {}, - ) - VoiceMessageDeleteButton( - enabled = false, - onClick = {}, - ) + IconButton(onClick = {}) { + VoiceMessageDeleteButtonIcon( + enabled = true, + ) + } + IconButton(onClick = {}) { + VoiceMessageDeleteButtonIcon( + enabled = false, + ) + } } } 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) 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 53% 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 32fe2847c2..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 @@ -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( +internal fun VoiceMessageRecorderButtonIcon( 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 { +internal fun VoiceMessageRecorderButtonIconPreview() = ElementPreview { Row { - VoiceMessageRecorderButton( - isRecording = false, - onEvent = {}, - ) - VoiceMessageRecorderButton( - isRecording = true, - onEvent = {}, - ) + IconButton(onClick = {}) { + VoiceMessageRecorderButtonIcon( + isRecording = false, + ) + } + IconButton(onClick = {}) { + VoiceMessageRecorderButtonIcon( + isRecording = true, + ) + } } } 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 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