[a11y] voice message improvements (#5980)
* A11Y: ensure a11y focus is not lost and reset to the back button when the user start playing a pending voice message. * 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. * Cleanup code. This if was not necessary. * Small rework to prepare a bugfix. No behavior / UI change. * Ensure that the keyboard focus and accessibility focus is not lost when deleting a pending voice message. * Update screenshots * Improve code readability. * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user