diff --git a/changelog.d/1261.feature b/changelog.d/1261.feature new file mode 100644 index 0000000000..bf7dd2399d --- /dev/null +++ b/changelog.d/1261.feature @@ -0,0 +1 @@ +[Rich text editor] Add formatting menu (accessible via the '+' button) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index 43805bb5c0..38ef458bd9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.BarChart import androidx.compose.material.icons.filled.Collections +import androidx.compose.material.icons.filled.FormatColorText import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.PhotoCamera import androidx.compose.material.icons.filled.Videocam @@ -145,6 +146,11 @@ internal fun AttachmentSourcePickerMenu( text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, ) } + ListItem( + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) }, + icon = { Icon(Icons.Default.FormatColorText, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) }, + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index 4bfd290a77..92b180f326 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -36,6 +36,7 @@ sealed interface MessageComposerEvents { data object Location : PickAttachmentSource data object Poll : PickAttachmentSource } + data class ToggleTextFormatting(val enabled: Boolean) : MessageComposerEvents data object CancelSendAttachment : MessageComposerEvents data class Error(val error: Throwable) : MessageComposerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 687e951933..3c3ebc2db9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -110,6 +110,7 @@ class MessageComposerPresenter @Inject constructor( val ongoingSendAttachmentJob = remember { mutableStateOf(null) } var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } + var showTextFormatting: Boolean by remember { mutableStateOf(false) } LaunchedEffect(messageComposerContext.composerMode) { when (val modeValue = messageComposerContext.composerMode) { @@ -190,6 +191,10 @@ class MessageComposerPresenter @Inject constructor( ongoingSendAttachmentJob.value == null } } + is MessageComposerEvents.ToggleTextFormatting -> { + showAttachmentSourcePicker = false + showTextFormatting = event.enabled + } is MessageComposerEvents.Error -> { analyticsService.trackError(event.error) } @@ -201,6 +206,7 @@ class MessageComposerPresenter @Inject constructor( isFullScreen = isFullScreen.value, mode = messageComposerContext.composerMode, showAttachmentSourcePicker = showAttachmentSourcePicker, + showTextFormatting = showTextFormatting, canShareLocation = canShareLocation.value, canCreatePoll = canCreatePoll.value, attachmentsState = attachmentsState.value, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index bdff621521..65fac53fdc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -28,6 +28,7 @@ data class MessageComposerState( val isFullScreen: Boolean, val mode: MessageComposerMode, val showAttachmentSourcePicker: Boolean, + val showTextFormatting: Boolean, val canShareLocation: Boolean, val canCreatePoll: Boolean, val attachmentsState: AttachmentsState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index ab15d8ccd8..d86969fc19 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -32,6 +32,7 @@ fun aMessageComposerState( composerState: RichTextEditorState = RichTextEditorState("", fake = true), isFullScreen: Boolean = false, mode: MessageComposerMode = MessageComposerMode.Normal(content = ""), + showTextFormatting: Boolean = false, showAttachmentSourcePicker: Boolean = false, canShareLocation: Boolean = true, canCreatePoll: Boolean = true, @@ -40,6 +41,7 @@ fun aMessageComposerState( richTextEditorState = composerState.apply { if(requestFocus) requestFocus() }, isFullScreen = isFullScreen, mode = mode, + showTextFormatting = showTextFormatting, showAttachmentSourcePicker = showAttachmentSourcePicker, canShareLocation = canShareLocation, canCreatePoll = canCreatePoll, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 5106a3359b..4ea5a7cf76 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -49,6 +49,10 @@ fun MessageComposerView( state.eventSink(MessageComposerEvents.CloseSpecialMode) } + fun onDismissTextFormatting() { + state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = false)) + } + fun onError(error: Throwable) { state.eventSink(MessageComposerEvents.Error(error)) } @@ -66,8 +70,10 @@ fun MessageComposerView( onRequestFocus = { state.richTextEditorState.requestFocus() }, onSendMessage = ::sendMessage, composerMode = state.mode, + showTextFormatting = state.showTextFormatting, onResetComposerMode = ::onCloseSpecialMode, onAddAttachment = ::onAddAttachment, + onDismissTextFormatting = ::onDismissTextFormatting, onError = ::onError, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 7284fb3b7e..d1d4a54073 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -539,6 +539,29 @@ class MessageComposerPresenterTest { } } + @Test + fun `present - ToggleTextFormatting toggles text formatting`() = runTest { + val presenter = createPresenter(this) + moleculeFlow(RecompositionMode.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.showTextFormatting).isFalse() + initialState.eventSink(MessageComposerEvents.AddAttachment) + val composerOptions = awaitItem() + assertThat(composerOptions.showAttachmentSourcePicker).isTrue() + composerOptions.eventSink(MessageComposerEvents.ToggleTextFormatting(true)) + awaitItem() // composer options closed + val showTextFormatting = awaitItem() + assertThat(showTextFormatting.showAttachmentSourcePicker).isFalse() + assertThat(showTextFormatting.showTextFormatting).isTrue() + showTextFormatting.eventSink(MessageComposerEvents.ToggleTextFormatting(false)) + val finished = awaitItem() + assertThat(finished.showTextFormatting).isFalse() + } + } + private suspend fun ReceiveTurbine.backToNormalMode(state: MessageComposerState, skipCount: Int = 0) { state.eventSink.invoke(MessageComposerEvents.CloseSpecialMode) skipItems(skipCount) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index 4f06b3ebcb..cd1b408a99 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -28,4 +28,16 @@ object VectorIcons { val Groups = R.drawable.ic_groups val Share = R.drawable.ic_share val EndPoll = R.drawable.ic_poll_end + val Bold = R.drawable.ic_bold + val BulletList = R.drawable.ic_bullet_list + val CodeBlock = R.drawable.ic_code_block + val IndentIncrease = R.drawable.ic_indent_increase + val IndentDecrease = R.drawable.ic_indent_decrease + val InlineCode = R.drawable.ic_inline_code + val Italic = R.drawable.ic_italic + val Link = R.drawable.ic_link + val NumberedList = R.drawable.ic_numbered_list + val Quote = R.drawable.ic_quote + val Strikethrough = R.drawable.ic_strikethrough + val Underline = R.drawable.ic_underline } diff --git a/libraries/designsystem/src/main/res/drawable/ic_bold.xml b/libraries/designsystem/src/main/res/drawable/ic_bold.xml new file mode 100644 index 0000000000..c361f85d3d --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_bold.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml b/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml new file mode 100644 index 0000000000..72d8324622 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_bullet_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_code_block.xml b/libraries/designsystem/src/main/res/drawable/ic_code_block.xml new file mode 100644 index 0000000000..6e622f5b27 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_code_block.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml b/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml new file mode 100644 index 0000000000..5a0b284223 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_indent_decrease.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml b/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml new file mode 100644 index 0000000000..367686ceac --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_indent_increase.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml b/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml new file mode 100644 index 0000000000..c0dc504ed9 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_inline_code.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_italic.xml b/libraries/designsystem/src/main/res/drawable/ic_italic.xml new file mode 100644 index 0000000000..f640c109f4 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_italic.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_link.xml b/libraries/designsystem/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000000..fd69ec4703 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_link.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml b/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml new file mode 100644 index 0000000000..f4f5862656 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_numbered_list.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_quote.xml b/libraries/designsystem/src/main/res/drawable/ic_quote.xml new file mode 100644 index 0000000000..e17565a6cc --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_quote.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml b/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml new file mode 100644 index 0000000000..d1994f8045 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_strikethrough.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_underline.xml b/libraries/designsystem/src/main/res/drawable/ic_underline.xml new file mode 100644 index 0000000000..09f92f2104 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_underline.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index 3aaae7ca6e..633491d3b3 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.constraintlayout.compose) implementation(libs.matrix.richtexteditor) api(libs.matrix.richtexteditor.compose) 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 04fa08c9ce..d137af18bb 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 @@ -18,29 +18,29 @@ package io.element.android.libraries.textcomposer import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -51,21 +51,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension.Companion.fillToConstraints +import androidx.constraintlayout.compose.Visibility import io.element.android.libraries.designsystem.VectorIcons -import io.element.android.libraries.designsystem.modifiers.applyIf import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.text.applyScaleUp import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.TransactionId @@ -73,12 +74,17 @@ import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.libraries.textcomposer.components.FormattingOption +import io.element.android.libraries.textcomposer.components.FormattingOptionState import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorDefaults import io.element.android.wysiwyg.compose.RichTextEditorState +import io.element.android.wysiwyg.view.models.InlineFormat import kotlinx.coroutines.android.awaitFrame +import uniffi.wysiwyg_composer.ActionState +import uniffi.wysiwyg_composer.ComposerAction @Composable fun TextComposer( @@ -86,112 +92,130 @@ fun TextComposer( composerMode: MessageComposerMode, canSendMessage: Boolean, modifier: Modifier = Modifier, + showTextFormatting: Boolean = false, onRequestFocus: () -> Unit = {}, onSendMessage: (Message) -> Unit = {}, onResetComposerMode: () -> Unit = {}, onAddAttachment: () -> Unit = {}, + onDismissTextFormatting: () -> Unit = {}, onError: (Throwable) -> Unit = {}, ) { - Row( - modifier.padding( - horizontal = 12.dp, - vertical = 8.dp - ), verticalAlignment = Alignment.Bottom - ) { - AttachmentButton(onClick = onAddAttachment, modifier = Modifier.padding(vertical = 6.dp)) - Spacer(modifier = Modifier.width(12.dp)) - val roundCornerSmall = 20.dp.applyScaleUp() - val roundCornerLarge = 28.dp.applyScaleUp() + val onSendClicked = { + onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown)) + } - val roundedCornerSize = remember(state.lineCount, composerMode) { - if (state.lineCount > 1 || composerMode is MessageComposerMode.Special) { - roundCornerSmall - } else { - roundCornerLarge - } - } - val roundedCornerSizeState = animateDpAsState( - targetValue = roundedCornerSize, - animationSpec = tween( - durationMillis = 100, + Column( + modifier = modifier + .padding( + start = 3.dp, + end = 6.dp, + top = 8.dp, + bottom = 5.dp, ) - ) - val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) - val minHeight = 42.dp.applyScaleUp() - val colors = ElementTheme.colors - val bgColor = colors.bgSubtleSecondary - - val borderColor by remember(state.hasFocus, colors) { - derivedStateOf { - if (state.hasFocus) colors.borderDisabled else bgColor - } - } - - Column( - modifier = Modifier - .fillMaxWidth() - .clip(roundedCorners) - .background(color = bgColor) - .border(1.dp, borderColor, roundedCorners) + .fillMaxWidth(), + ) { + ConstraintLayout( + modifier = Modifier.fillMaxWidth(), ) { - if (composerMode is MessageComposerMode.Special) { - ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) + val (composeOptions, textInput, sendButton) = createRefs() + val showComposerOptionsButton by remember(showTextFormatting) { + derivedStateOf { !showTextFormatting } } - val defaultTypography = ElementTheme.typography.fontBodyLgRegular - - Box { - Box( - modifier = Modifier - .heightIn(min = minHeight) - .background(color = bgColor, shape = roundedCorners) - .padding( - PaddingValues( - top = 4.dp.applyScaleUp(), - bottom = 4.dp.applyScaleUp(), - start = 12.dp.applyScaleUp(), - end = 42.dp.applyScaleUp() - ) - ), - contentAlignment = Alignment.CenterStart, - ) { - - // Placeholder - if (state.messageHtml.isEmpty()) { - Text( - stringResource(CommonStrings.common_message), - style = defaultTypography.copy( - color = ElementTheme.colors.textDisabled, - ), - ) - } - - RichTextEditor( - state = state, - modifier = Modifier - .fillMaxWidth(), - style = RichTextEditorDefaults.style( - text = RichTextEditorDefaults.textStyle( - color = if (state.hasFocus) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.secondary - } - ), - cursor = RichTextEditorDefaults.cursorStyle( - color = ElementTheme.colors.iconAccentTertiary, - ) - ), - onError = onError - ) - } - - SendButton( - canSendMessage = canSendMessage, - onClick = { onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown)) }, - composerMode = composerMode, - modifier = Modifier.padding(end = 6.dp.applyScaleUp(), bottom = 6.dp.applyScaleUp()) + IconButton( + modifier = Modifier + .size(48.dp) + .constrainAs(composeOptions) { + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + visibility = if (showComposerOptionsButton) Visibility.Visible else Visibility.Gone + }, + onClick = onAddAttachment + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = R.drawable.ic_plus, // TODO Replace with design system icon when available + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), + tint = ElementTheme.colors.iconPrimary, ) } + val roundCornerSmall = 20.dp.applyScaleUp() + val roundCornerLarge = 28.dp.applyScaleUp() + + val roundedCornerSize = remember(state.lineCount, composerMode) { + if (state.lineCount > 1 || composerMode is MessageComposerMode.Special) { + roundCornerSmall + } else { + roundCornerLarge + } + } + val roundedCornerSizeState = animateDpAsState( + targetValue = roundedCornerSize, + animationSpec = tween( + durationMillis = 100, + ) + ) + val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) + val colors = ElementTheme.colors + val bgColor = colors.bgSubtleSecondary + + val borderColor by remember(state.hasFocus, colors) { + derivedStateOf { + if (state.hasFocus) colors.borderDisabled else bgColor + } + } + + Column( + modifier = Modifier + .constrainAs(textInput) { + start.linkTo(composeOptions.end, margin = 3.dp, goneMargin = 9.dp) + end.linkTo(sendButton.start, margin = 6.dp, goneMargin = 6.dp) + bottom.linkTo(parent.bottom) + width = fillToConstraints + } + .padding(vertical = 3.dp) + .fillMaxWidth() + .clip(roundedCorners) + .background(color = bgColor) + .border(1.dp, borderColor, roundedCorners) + ) { + if (composerMode is MessageComposerMode.Special) { + ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) + } + + TextInput( + state = state, + roundedCorners = roundedCorners, + bgColor = bgColor, + onError = onError, + ) + } + + SendButton( + canSendMessage = canSendMessage, + onClick = onSendClicked, + composerMode = composerMode, + modifier = Modifier + .constrainAs(sendButton) { + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + visibility = if (!showTextFormatting) Visibility.Visible else Visibility.Gone + } + ) + } + + if (showTextFormatting) { + TextFormatting( + state = state, + onDismiss = onDismissTextFormatting, + sendButton = { + SendButton( + canSendMessage = canSendMessage, + onClick = onSendClicked, + composerMode = composerMode, + modifier = it + ) + }, + ) } } @@ -208,6 +232,192 @@ fun TextComposer( } } +@Composable +private fun TextInput( + state: RichTextEditorState, + roundedCorners: RoundedCornerShape, + bgColor: Color, + modifier: Modifier = Modifier, + onError: (Throwable) -> Unit = {}, +) { + val minHeight = 42.dp.applyScaleUp() + val defaultTypography = ElementTheme.typography.fontBodyLgRegular + Box( + modifier = modifier + .heightIn(min = minHeight) + .background(color = bgColor, shape = roundedCorners) + .padding( + PaddingValues( + top = 4.dp.applyScaleUp(), + bottom = 4.dp.applyScaleUp(), + start = 12.dp.applyScaleUp(), + end = 42.dp.applyScaleUp() + ) + ), + contentAlignment = Alignment.CenterStart, + ) { + + // Placeholder + if (state.messageHtml.isEmpty()) { + Text( + stringResource(CommonStrings.common_message), + style = defaultTypography.copy( + color = ElementTheme.colors.textDisabled, + ), + ) + } + + RichTextEditor( + state = state, + modifier = Modifier + .fillMaxWidth(), + style = RichTextEditorDefaults.style( + text = RichTextEditorDefaults.textStyle( + color = if (state.hasFocus) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.secondary + } + ), + cursor = RichTextEditorDefaults.cursorStyle( + color = ElementTheme.colors.iconAccentTertiary, + ) + ), + onError = onError + ) + } +} + +@Composable +private fun TextFormatting( + state: RichTextEditorState, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + sendButton: @Composable (modifier: Modifier) -> Unit, +) { + ConstraintLayout( + modifier = modifier + .fillMaxWidth() + ) { + val (close, formatting, send) = createRefs() + + IconButton( + modifier = Modifier + .size(48.dp) + .constrainAs(close) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + }, + onClick = onDismiss + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = R.drawable.ic_cancel, // TODO Replace with design system icon when available + contentDescription = stringResource(CommonStrings.action_close), + tint = ElementTheme.colors.iconPrimary, + ) + } + + val scrollState = rememberScrollState() + Row( + modifier = Modifier + .constrainAs(formatting) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(close.end, margin = 3.dp) + end.linkTo(send.start, margin = 20.dp) + width = fillToConstraints + } + .horizontalScroll(scrollState), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FormattingOption( + state = state.actions[ComposerAction.BOLD].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.Bold) }, + imageVector = ImageVector.vectorResource(VectorIcons.Bold), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_bold) + ) + FormattingOption( + state = state.actions[ComposerAction.ITALIC].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.Italic) }, + imageVector = ImageVector.vectorResource(VectorIcons.Italic), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_italic) + ) + FormattingOption( + state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.Underline) }, + imageVector = ImageVector.vectorResource(VectorIcons.Underline), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_underline) + ) + FormattingOption( + state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.StrikeThrough) }, + imageVector = ImageVector.vectorResource(VectorIcons.Strikethrough), + contentDescription = stringResource(CommonStrings.rich_text_editor_format_strikethrough) + ) + FormattingOption( + state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + onClick = { state.toggleList(ordered = false) }, + imageVector = ImageVector.vectorResource(VectorIcons.BulletList), + contentDescription = stringResource(CommonStrings.rich_text_editor_bullet_list) + ) + FormattingOption( + state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + onClick = { state.toggleList(ordered = true) }, + imageVector = ImageVector.vectorResource(VectorIcons.NumberedList), + contentDescription = stringResource(CommonStrings.rich_text_editor_numbered_list) + ) + FormattingOption( + state = state.actions[ComposerAction.INDENT].toButtonState(), + onClick = { state.indent() }, + imageVector = ImageVector.vectorResource(VectorIcons.IndentIncrease), + contentDescription = stringResource(CommonStrings.rich_text_editor_indent) + ) + FormattingOption( + state = state.actions[ComposerAction.UNINDENT].toButtonState(), + onClick = { state.unindent() }, + imageVector = ImageVector.vectorResource(VectorIcons.IndentDecrease), + contentDescription = stringResource(CommonStrings.rich_text_editor_unindent) + ) + FormattingOption( + state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + onClick = { state.toggleInlineFormat(InlineFormat.InlineCode) }, + imageVector = ImageVector.vectorResource(VectorIcons.InlineCode), + contentDescription = stringResource(CommonStrings.rich_text_editor_inline_code) + ) + FormattingOption( + state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + onClick = { state.toggleCodeBlock() }, + imageVector = ImageVector.vectorResource(VectorIcons.CodeBlock), + contentDescription = stringResource(CommonStrings.rich_text_editor_code_block) + ) + FormattingOption( + state = state.actions[ComposerAction.QUOTE].toButtonState(), + onClick = { state.toggleQuote() }, + imageVector = ImageVector.vectorResource(VectorIcons.Quote), + contentDescription = stringResource(CommonStrings.rich_text_editor_quote) + ) + } + + sendButton( + Modifier.constrainAs(send) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + end.linkTo(parent.end) + }, + ) + } +} + +private fun ActionState?.toButtonState(): FormattingOptionState = + when (this) { + ActionState.ENABLED -> FormattingOptionState.Default + ActionState.REVERSED -> FormattingOptionState.Selected + ActionState.DISABLED, null -> FormattingOptionState.Disabled + } + @Composable private fun ComposerModeView( composerMode: MessageComposerMode, @@ -341,53 +551,17 @@ private fun ReplyToModeView( } @Composable -private fun AttachmentButton( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Surface( - modifier - .size(30.dp.applyScaleUp()) - .clickable(onClick = onClick), - shape = CircleShape, - color = ElementTheme.colors.iconPrimary - ) { - Image( - modifier = Modifier.size(12.5f.dp.applyScaleUp()), - painter = painterResource(R.drawable.ic_add_attachment), - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - contentScale = ContentScale.Inside, - colorFilter = ColorFilter.tint( - LocalContentColor.current - ) - ) - } -} - -@Composable -private fun BoxScope.SendButton( +private fun SendButton( canSendMessage: Boolean, onClick: () -> Unit, composerMode: MessageComposerMode, modifier: Modifier = Modifier, ) { - val interactionSource = remember { MutableInteractionSource() } - Box( + IconButton( modifier = modifier - .clip(CircleShape) - .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) - .size(30.dp.applyScaleUp()) - .align(Alignment.BottomEnd) - .applyIf(composerMode !is MessageComposerMode.Edit, ifTrue = { - padding(start = 1.dp.applyScaleUp()) // Center the arrow in the circle - }) - .clickable( - enabled = canSendMessage, - interactionSource = interactionSource, - indication = rememberRipple(bounded = false), - onClick = onClick, - ), - contentAlignment = Alignment.Center, + .size(48.dp.applyScaleUp()), + onClick = onClick, + enabled = canSendMessage, ) { val iconId = when (composerMode) { is MessageComposerMode.Edit -> R.drawable.ic_tick @@ -397,13 +571,22 @@ private fun BoxScope.SendButton( is MessageComposerMode.Edit -> stringResource(CommonStrings.action_edit) else -> stringResource(CommonStrings.action_send) } - Icon( - modifier = Modifier.size(16.dp.applyScaleUp()), - resourceId = iconId, - contentDescription = contentDescription, - // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary - tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled - ) + Box( + modifier = Modifier + .clip(CircleShape) + .size(36.dp.applyScaleUp()) + .background(if (canSendMessage) ElementTheme.colors.iconAccentTertiary else Color.Transparent) + ) { + Icon( + modifier = Modifier + .height(18.dp.applyScaleUp()) + .align(Alignment.Center), + resourceId = iconId, + contentDescription = contentDescription, + // Exception here, we use Color.White instead of ElementTheme.colors.iconOnSolidPrimary + tint = if (canSendMessage) Color.White else ElementTheme.colors.iconDisabled + ) + } } } @@ -447,6 +630,31 @@ internal fun TextComposerSimplePreview() = ElementPreview { } } +@DayNightPreviews +@Composable +internal fun TextComposerFormattingPreview() = ElementPreview { + Column { + TextComposer( + RichTextEditorState("", fake = true), + canSendMessage = false, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + ) + TextComposer( + RichTextEditorState("A message", fake = true), + canSendMessage = true, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + ) + TextComposer( + RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true), + canSendMessage = true, + showTextFormatting = true, + composerMode = MessageComposerMode.Normal(""), + ) + } +} + @DayNightPreviews @Composable internal fun TextComposerEditPreview() = ElementPreview { diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt new file mode 100644 index 0000000000..7eb1d08293 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOption.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.theme.compound.generated.SemanticColors + +@Composable +internal fun FormattingOption( + state: FormattingOptionState, + onClick: () -> Unit, + imageVector: ImageVector, + contentDescription: String, + modifier: Modifier = Modifier, + colors: SemanticColors = ElementTheme.colors, +) { + val backgroundColor = when (state) { + FormattingOptionState.Selected -> colors.bgActionPrimaryRest + FormattingOptionState.Default, + FormattingOptionState.Disabled -> Color.Transparent + } + + val foregroundColor = when (state) { + FormattingOptionState.Selected -> colors.iconOnSolidPrimary + FormattingOptionState.Default -> colors.iconPrimary + FormattingOptionState.Disabled -> colors.iconDisabled + } + Box( + modifier = modifier + .clickable { onClick() } + .size(44.dp.applyScaleUp()) + .background(backgroundColor, shape = RoundedCornerShape(4.dp.applyScaleUp())) + ) { + Icon( + modifier = Modifier.align(Alignment.Center), + imageVector = imageVector, + contentDescription = contentDescription, + tint = foregroundColor, + ) + } +} + diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOptionState.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOptionState.kt new file mode 100644 index 0000000000..386fa5a668 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/FormattingOptionState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.textcomposer.components + +internal enum class FormattingOptionState { + Default, Selected, Disabled +} + diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_add_attachment.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_add_attachment.xml deleted file mode 100644 index ac9d53639b..0000000000 --- a/libraries/textcomposer/impl/src/main/res/drawable/ic_add_attachment.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml new file mode 100644 index 0000000000..5c27ba82d9 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_cancel.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml new file mode 100644 index 0000000000..ead38721dc --- /dev/null +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_plus.xml @@ -0,0 +1,9 @@ + + + diff --git a/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml b/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml index 64e0f120c4..bf346b3a01 100644 --- a/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml +++ b/libraries/textcomposer/impl/src/main/res/drawable/ic_send.xml @@ -1,9 +1,9 @@ + android:width="21dp" + android:height="18dp" + android:viewportWidth="21" + android:viewportHeight="18"> + android:pathData="M20.252,10.085 L4.681,17.867c-1.049,0.525 -2.141,-0.601 -1.628,-1.627 0,0 1.93,-3.897 2.461,-4.918 0.531,-1.021 1.138,-1.197 6.781,-1.927 0.209,-0.027 0.38,-0.185 0.38,-0.395 0,-0.21 -0.171,-0.368 -0.38,-0.395C6.652,7.876 6.045,7.699 5.514,6.678 4.983,5.658 3.053,1.76 3.053,1.76 2.54,0.734 3.632,-0.391 4.681,0.133L20.252,7.915c0.894,0.446 0.894,1.723 0,2.17z" + android:fillColor="@android:color/white"/> diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png index 58ebf07374..efcd596b40 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-D-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79aeef6875265e119c3b4b97cea4d36ba3354ae52c4b94b69bbc09461b7bc319 -size 22259 +oid sha256:e67b171ca09fd2efb25338a20fe7af7464e0e1ecb1b02afd679dbfc7cd3cd7df +size 25646 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png index 6e91b56f10..a385dd8498 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_AttachmentSourcePickerMenu-N-1_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8dafa9a97ebc77f00fdb0432c7b94272f6ea1873c3475353be47ecde95e8b057 -size 20670 +oid sha256:e92e96cc117bf42ffb9fa282bcb9e40fd51c414e1d432862940c0f8fb6600fd0 +size 23453 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png index fa68f3bd24..ee0937d439 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4013454094701004f3c132df1ea1c3db2aa56a025b9edea80417fdd16fe55b19 -size 10422 +oid sha256:f3ea303577f655368800debe9c40e1292dc08a20da7f2ea6ddddb87f8407b112 +size 10431 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png index f6040d0e1a..e537e910e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db74b1655e377e4fd8c35aa18c22ef5f5641586b64d5e582ab2079afeac4b8c3 -size 10775 +oid sha256:b646c06e55b50b64eb7b566fbc0bcafa7f7e348398f87152d008a47e7448f4e0 +size 10736 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png index 2a10272dac..00cfa0d87e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfc6f54988e4fab08ff443f0c2ea285d47e3d030a8e6c92a1294ee0f85dc1269 -size 51967 +oid sha256:ba052f24e62aabc015da4b38bb893148c888a858a097669b2191e7880867e371 +size 52114 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png index 5cf07497cd..57d6580a12 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2ffd4fc93e7da38836fd860ce5e505c5f0c25fdd6558efef480aaeb05d00dd5 -size 53327 +oid sha256:4f8f7fd27e56aa2ca42053c249353836187467b99a9225c93c22f38365e63ccb +size 53285 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png index 41f0170682..8a4f9bc201 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96864552336c65b464251c262f330856fc82c69a5f92f89b1357a185d654fccd -size 52252 +oid sha256:fbfc5593de6c7dda915b3a8bc881f0485b4b8400f52412547aa914862d1d8840 +size 52245 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png index a022fb7675..6907cccce9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2411212e87f95415e0b4ed6732387922faed891cde5e78fa9f0691a185873129 -size 51026 +oid sha256:d56d5666d1d7749484f19c983e3cb2125110039c80e5898684e20b7e24a3e56b +size 51312 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png index 5fe74737cf..b2e621fc71 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewDark_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:255560f478c093f4ca7d021646f4cf9064c16b420d5eeec7cbce97d26c751d92 -size 49540 +oid sha256:db0107e648e2acfc253d83f5e3b2f3e41de1abc9bdc4922dfd923ad4e50b8f5f +size 49700 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png index caf6d5a3da..1746cfeb84 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a384aa84ae514aa4989d4b4a5ef8048cb8810f1f37056137c77fd0ee798a0bab -size 53941 +oid sha256:398f4b1c7b21ab4c57802fb724d62b8c904f5a0c700eba507a811cb58881df49 +size 53896 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png index 93163a476e..69a8b67d95 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a48badf15c59ab5c42fc7cc200971f00cefa7838461ad0cb59537c2b4065bfff -size 55409 +oid sha256:2913fc7ee2c4b32e79e0d813ee8fa54e6d3f863466b798b8194462cbd9ef4fde +size 55137 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png index 1ff65d3301..f8b59bb375 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88a498b9070653513b3e27aa2b597367ee85c8526c6e4c3712a4cb0c06e5313b -size 54353 +oid sha256:a20479945a552e2d3d3fb309c07ac7b9a66d4dfac0ad1ddf2c86c40fa0d34db9 +size 54175 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png index be91f54717..367b0dc907 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf493a9cb7325b4f68924abd86864b37c72226afcf1c4179a06c17e923cfd0ae -size 55837 +oid sha256:1e7697c3f1eb995fc0bbf084853fce1cc10e841d0275d6ff4f2d857afd81b31d +size 56103 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png index 432e114fd5..cbdda6b794 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesViewLight_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ac3dbf36380fa273d82ebe8836c525bbeee004d6348822d965b216f9bcdb920 -size 51335 +oid sha256:7e162503c27609f01e05bb2c634c2d3123abf6e786ecfe763c902d400ead050f +size 51283 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-1_2_null,NEXUS_5,1.0,en].png deleted file mode 100644 index b1144213da..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-1_2_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ce08e8c81d5494d2d803da43612a137df881e359646878b1d38cdfe77e0857ff -size 13829 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9d906ebe35 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46683edccf9c7686a07d7098c4387f296556ad2678381aad35efe2787cf6ad0a +size 14173 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-1_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 62264fcf27..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-1_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3c2ba0ac13c81c707f0d00e3ff69737163dfc450a3d21aa468dd0b34deeeb7e1 -size 12916 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4b9d77d2b1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6485de8235af05a9815a558b1af03b51e8c4b30ad0372aea0aa9fb2303525c9b +size 13286 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..760cf75800 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0e4c8da669ee5383a7d0f828a3300b923ec38ab5e04bc388da0e83f1cf1ccf3 +size 38291 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0b9f9f3ab7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_3_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f26aa7b13403cfaa864778a6ec5c26edde5932f3d887c7148d3a2b9f60f6e6a +size 36392 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-2_3_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 3e872eda82..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-2_3_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f15bc37e2a3d052e8d79e663f2c03223c27ddff3c02cd69f95a140c33fc6952c -size 79320 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..37e8a8561f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_4_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e65e15c721a939ed700719cacbdee57e1ccb89d984e88bdb5ed772c4b583472a +size 81494 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-2_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-2_4_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 8bd9e09c69..0000000000 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-2_4_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6aa9b8849a2bbd7b487de6fee7fd355300091bc14d647973c4beea02e6009ac4 -size 76465 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..466f321959 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49f6d3b7f93008640abf30a5a65ddca6845e16343ef97c3450faf9dd6d9b9cec +size 78788 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png index 8afc6fc94a..d6beb7c22c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:501734b297cec566a008d3492690cf75b46ab68e662d2835a0b195fd0740c13f -size 42912 +oid sha256:2a998d3db9e4454fe74f31028b1dc61e4d6c07c824192f5e75563234dedf0b26 +size 44108 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png index de57e08951..863e8b8628 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff69ed7056380df3279a39b7544bc1887d2e455b43fb069b547e32cf5c92dc0f -size 40229 +oid sha256:97ab0a0b64ea7704a1557fc34b24d7caea34f9951948f5f5637f0bda596288dd +size 41455