From 16d0ff1e663eb9c8df20f2e4928bcb8add303373 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Mon, 18 Sep 2023 21:04:17 +0100 Subject: [PATCH 1/7] Add RTE full screen mode --- .../impl/ExpandableBottomSheetScaffold.kt | 152 ++++ .../messages/impl/MessagesStateProvider.kt | 5 +- .../features/messages/impl/MessagesView.kt | 75 +- .../MessageComposerPresenter.kt | 4 +- .../messagecomposer/MessageComposerState.kt | 4 +- .../MessageComposerStateProvider.kt | 5 +- .../messagecomposer/MessageComposerView.kt | 35 +- .../MessageComposerPresenterTest.kt | 43 +- gradle/libs.versions.toml | 2 +- .../theme/components/BottomSheetDragHandle.kt | 73 ++ libraries/textcomposer/impl/build.gradle.kts | 2 - .../libraries/textcomposer/TextComposer.kt | 771 ++++++++++-------- .../components/TextInputRoundedCornerShape.kt | 48 ++ 13 files changed, 798 insertions(+), 421 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt create mode 100644 libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt new file mode 100644 index 0000000000..884895e2bb --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt @@ -0,0 +1,152 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class) + +package io.element.android.features.messages.impl + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.material3.BottomSheetScaffold +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState +import androidx.compose.material3.SheetValue +import androidx.compose.material3.rememberBottomSheetScaffoldState +import androidx.compose.material3.rememberStandardBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min +import kotlin.math.roundToInt + +/** + * A [BottomSheetScaffold] that allows the sheet to be expanded the screen height + * of the sheet contents. + * + * @param content The main content. + * @param sheetContent The sheet content. + * @param sheetDragHandle The drag handle for the sheet. + * @param sheetSwipeEnabled Whether the sheet can be swiped. + * @param sheetShape The shape of the sheet. + * @param sheetTonalElevation The tonal elevation of the sheet. + * @param sheetShadowElevation The shadow elevation of the sheet. + * @param modifier The modifier for the layout. + * @param sheetContentKey The key for the sheet content. If the key changes, the sheet will be remeasured. + */ +@Composable +internal fun ExpandableBottomSheetScaffold( + content: @Composable (padding: PaddingValues) -> Unit, + sheetContent: @Composable (subcomposing: Boolean) -> Unit, + sheetDragHandle: @Composable () -> Unit, + sheetSwipeEnabled: Boolean, + sheetShape: Shape, + sheetTonalElevation: Dp, + sheetShadowElevation: Dp, + modifier: Modifier = Modifier, + sheetContentKey: Int? = null, +) { + val scaffoldState = rememberBottomSheetScaffoldState( + bottomSheetState = rememberStandardBottomSheetState( + initialValue = SheetValue.PartiallyExpanded, + skipHiddenState = true, + ) + ) + LaunchedEffect(sheetSwipeEnabled) { + if (!sheetSwipeEnabled) { + scaffoldState.bottomSheetState.partialExpand() + } + } + val scaffold: @Composable (sheetContent: @Composable () -> Unit, dragHandle: @Composable () -> Unit, peekHeight: Dp) -> Unit = + { sheetContent, dragHandle, peekHeight -> + BottomSheetScaffold( + modifier = Modifier, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetSwipeEnabled = sheetSwipeEnabled, + sheetDragHandle = dragHandle, + sheetShape = sheetShape, + content = content, + sheetContent = { sheetContent() }, + sheetTonalElevation = sheetTonalElevation, + sheetShadowElevation = sheetShadowElevation, + ) + } + SubcomposeLayout( + modifier = modifier, + measurePolicy = { constraints: Constraints -> + val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(subcomposing = true) }.map { + it.measure(Constraints(maxWidth = constraints.maxWidth)) + }.first() + val dragHandleSub = subcompose(Slot.DragHandle) { sheetDragHandle() }.map { + it.measure(Constraints(maxWidth = constraints.maxWidth)) + }.firstOrNull() + val dragHandleHeight = dragHandleSub?.height?.toDp() ?: 0.dp + + val peekHeight = min( + constraints.maxHeight.toDp(), // prevent the sheet from expanding beyond the screen + sheetContentSub.height.toDp() + dragHandleHeight + ) + + val scaffoldPlaceables = subcompose(Slot.Scaffold) { + scaffold({ + Layout( + modifier = Modifier.fillMaxHeight(), + measurePolicy = { measurables, constraints -> + val maxHeight = constraints.maxHeight + val offset = scaffoldState.bottomSheetState.getOffset() ?: 0 + val height = Integer.max(0, maxHeight - offset) + val top = measurables[0].measure( + constraints.copy( + minHeight = height, + maxHeight = height + ) + ) + layout(constraints.maxWidth, constraints.maxHeight) { + top.place(x = 0, y = 0) + } + }, + content = { sheetContent(subcomposing = false) }) + }, sheetDragHandle, peekHeight) + }.map { measurable: Measurable -> + measurable.measure(constraints) + } + val scaffoldPlaceable = scaffoldPlaceables.first() + layout(constraints.maxWidth, constraints.maxHeight) { + scaffoldPlaceable.place(0, 0) + } + }) +} + +private fun SheetState.getOffset(): Int? = try { + requireOffset().roundToInt() +} catch (e: IllegalStateException) { + null +} + +private sealed class Slot { + data class SheetContent(val key: Int?): Slot() + data object DragHandle: Slot() + data object Scaffold: Slot() +} + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index a88ebcbcd8..5a430c7286 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -39,6 +39,7 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState(), aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), + aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)), aMessagesState().copy(userHasPermissionToSendMessage = false), aMessagesState().copy(showReinvitePrompt = true), aMessagesState().copy( @@ -55,9 +56,7 @@ fun aMessagesState() = MessagesState( userHasPermissionToSendMessage = true, userHasPermissionToRedact = false, composerState = aMessageComposerState().copy( - richTextEditorState = RichTextEditorState("Hello", fake = true).apply { - requestFocus() - }, + richTextEditorState = RichTextEditorState("Hello", initialFocus = true), isFullScreen = false, mode = MessageComposerMode.Normal("Hello"), ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 8cb5bff907..6602b72267 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -19,8 +19,8 @@ package io.element.android.features.messages.impl import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -32,13 +32,13 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle @@ -73,6 +73,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar @@ -86,7 +87,6 @@ import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import timber.log.Timber -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun MessagesView( state: MessagesState, @@ -277,40 +277,49 @@ private fun MessagesViewContent( modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, ) { - Column( + Box( modifier = modifier .fillMaxSize() .navigationBarsPadding() - .imePadding() + .imePadding(), ) { - // Hide timeline if composer is full screen - if (!state.composerState.isFullScreen) { - TimelineView( - state = state.timelineState, - modifier = Modifier.weight(1f), - onMessageClicked = onMessageClicked, - onMessageLongClicked = onMessageLongClicked, - onUserDataClicked = onUserDataClicked, - onTimestampClicked = onTimestampClicked, - onReactionClicked = onReactionClicked, - onReactionLongClicked = onReactionLongClicked, - onMoreReactionsClicked = onMoreReactionsClicked, - onSwipeToReply = onSwipeToReply, - ) - } - if (state.userHasPermissionToSendMessage) { - MessageComposerView( - state = state.composerState, - onSendLocationClicked = onSendLocationClicked, - onCreatePollClicked = onCreatePollClicked, - enableTextFormatting = state.enableTextFormatting, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(Alignment.Bottom) - ) - } else { - CantSendMessageBanner() - } + ExpandableBottomSheetScaffold( + sheetDragHandle = if (state.composerState.showTextFormatting) ({ BottomSheetDragHandle() }) else ({}), + sheetSwipeEnabled = state.composerState.showTextFormatting, + sheetShape = if(state.composerState.showTextFormatting) MaterialTheme.shapes.large else RectangleShape, + content = { paddingValues -> + TimelineView( + modifier = Modifier.padding(paddingValues), + state = state.timelineState, + onMessageClicked = onMessageClicked, + onMessageLongClicked = onMessageLongClicked, + onUserDataClicked = onUserDataClicked, + onTimestampClicked = onTimestampClicked, + onReactionClicked = onReactionClicked, + onReactionLongClicked = onReactionLongClicked, + onMoreReactionsClicked = onMoreReactionsClicked, + onSwipeToReply = onSwipeToReply, + ) + }, + sheetContent = { subcomposing: Boolean -> + if (state.userHasPermissionToSendMessage) { + MessageComposerView( + state = state.composerState, + subcomposing = subcomposing, + onSendLocationClicked = onSendLocationClicked, + onCreatePollClicked = onCreatePollClicked, + enableTextFormatting = state.enableTextFormatting, + modifier = Modifier + .fillMaxWidth(), + ) + } else { + CantSendMessageBanner() + } + }, + sheetContentKey = state.composerState.richTextEditorState.lineCount, + sheetTonalElevation = 0.dp, + sheetShadowElevation = 0.dp, + ) } } 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 d6e74a0df0..30747786b8 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 @@ -155,7 +155,9 @@ class MessageComposerPresenter @Inject constructor( when (event) { MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value MessageComposerEvents.CloseSpecialMode -> { - richTextEditorState.setHtml("") + localCoroutineScope.launch { + richTextEditorState.setHtml("") + } messageComposerContext.composerMode = MessageComposerMode.Normal("") } is MessageComposerEvents.SendMessage -> appCoroutineScope.sendMessage( 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 65fac53fdc..ff74a81abe 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 @@ -17,12 +17,13 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.ImmutableList -@Immutable +@Stable data class MessageComposerState( val richTextEditorState: RichTextEditorState, val isFullScreen: Boolean, @@ -34,7 +35,6 @@ data class MessageComposerState( val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit, ) { - val canSendMessage: Boolean = richTextEditorState.messageHtml.isNotEmpty() val hasFocus: Boolean = richTextEditorState.hasFocus } 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 d86969fc19..0483d945af 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 @@ -28,8 +28,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider Unit, onCreatePollClicked: () -> Unit, enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { - fun onFullscreenToggle() { - state.eventSink(MessageComposerEvents.ToggleFullScreenState) - } - fun sendMessage(message: Message) { state.eventSink(MessageComposerEvents.SendMessage(message)) } @@ -57,6 +58,13 @@ fun MessageComposerView( state.eventSink(MessageComposerEvents.Error(error)) } + val coroutineScope = rememberCoroutineScope() + fun onRequestFocus() { + coroutineScope.launch { + state.richTextEditorState.requestFocus() + } + } + Box(modifier = modifier) { AttachmentsBottomSheet( state = state, @@ -67,8 +75,8 @@ fun MessageComposerView( TextComposer( state = state.richTextEditorState, - canSendMessage = state.canSendMessage, - onRequestFocus = { state.richTextEditorState.requestFocus() }, + subcomposing = subcomposing, + onRequestFocus = ::onRequestFocus, onSendMessage = ::sendMessage, composerMode = state.mode, showTextFormatting = state.showTextFormatting, @@ -84,10 +92,13 @@ fun MessageComposerView( @PreviewsDayNight @Composable internal fun MessageComposerViewPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) = ElementPreview { - MessageComposerView( - state = state, - onSendLocationClicked = {}, - onCreatePollClicked = {}, - enableTextFormatting = true, - ) + Box(modifier = Modifier.height(200.dp)) { + MessageComposerView( + state = state, + onSendLocationClicked = {}, + onCreatePollClicked = {}, + enableTextFormatting = true, + subcomposing = false, + ) + } } 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 b87fe16aad..a0b0c68810 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 @@ -19,6 +19,7 @@ package io.element.android.features.messages.textcomposer import android.net.Uri +import androidx.compose.runtime.remember import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.ReceiveTurbine @@ -102,7 +103,6 @@ class MessageComposerPresenterTest { assertThat(initialState.showAttachmentSourcePicker).isFalse() assertThat(initialState.canShareLocation).isTrue() assertThat(initialState.attachmentsState).isEqualTo(AttachmentsState.None) - assertThat(initialState.canSendMessage).isFalse() } } @@ -132,13 +132,9 @@ class MessageComposerPresenterTest { skipItems(1) val initialState = awaitItem() initialState.richTextEditorState.setHtml(A_MESSAGE) - val withMessageState = awaitItem() - assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() - withMessageState.richTextEditorState.setHtml("") - val withEmptyMessageState = awaitItem() - assertThat(withEmptyMessageState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(withEmptyMessageState.canSendMessage).isFalse() + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) + initialState.richTextEditorState.setHtml("") + assertThat(initialState.richTextEditorState.messageHtml).isEqualTo("") } } @@ -146,7 +142,8 @@ class MessageComposerPresenterTest { fun `present - change mode to edit`() = runTest { val presenter = createPresenter(this) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) var state = awaitItem() @@ -156,7 +153,6 @@ class MessageComposerPresenterTest { assertThat(state.mode).isEqualTo(mode) state = awaitItem() assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(state.canSendMessage).isTrue() backToNormalMode(state, skipCount = 1) } } @@ -174,7 +170,6 @@ class MessageComposerPresenterTest { state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.richTextEditorState.messageHtml).isEqualTo("") - assertThat(state.canSendMessage).isFalse() backToNormalMode(state) } } @@ -192,7 +187,6 @@ class MessageComposerPresenterTest { state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.richTextEditorState.messageHtml).isEqualTo("") - assertThat(state.canSendMessage).isFalse() backToNormalMode(state) } } @@ -201,18 +195,17 @@ class MessageComposerPresenterTest { fun `present - send message`() = runTest { val presenter = createPresenter(this) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) val initialState = awaitItem() initialState.richTextEditorState.setHtml(A_MESSAGE) val withMessageState = awaitItem() assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_MESSAGE.toMessage())) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() waitForPredicate { analyticsService.capturedEvents.size == 1 } assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -233,7 +226,8 @@ class MessageComposerPresenterTest { fakeMatrixRoom, ) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) val initialState = awaitItem() @@ -244,7 +238,6 @@ class MessageComposerPresenterTest { val withMessageState = awaitItem() assertThat(withMessageState.mode).isEqualTo(mode) assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE) val withEditedMessageState = awaitItem() assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE) @@ -252,7 +245,6 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -273,7 +265,8 @@ class MessageComposerPresenterTest { fakeMatrixRoom, ) moleculeFlow(RecompositionMode.Immediate) { - presenter.present() + val state = presenter.present() + remember(state, state.richTextEditorState.messageHtml) { state } }.test { skipItems(1) val initialState = awaitItem() @@ -284,7 +277,6 @@ class MessageComposerPresenterTest { val withMessageState = awaitItem() assertThat(withMessageState.mode).isEqualTo(mode) assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_MESSAGE) - assertThat(withMessageState.canSendMessage).isTrue() withMessageState.richTextEditorState.setHtml(ANOTHER_MESSAGE) val withEditedMessageState = awaitItem() assertThat(withEditedMessageState.richTextEditorState.messageHtml).isEqualTo(ANOTHER_MESSAGE) @@ -292,7 +284,6 @@ class MessageComposerPresenterTest { skipItems(1) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() assertThat(fakeMatrixRoom.editMessageCalls.first()).isEqualTo(ANOTHER_MESSAGE to ANOTHER_MESSAGE) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -323,16 +314,11 @@ class MessageComposerPresenterTest { val state = awaitItem() assertThat(state.mode).isEqualTo(mode) assertThat(state.richTextEditorState.messageHtml).isEqualTo("") - assertThat(state.canSendMessage).isFalse() state.richTextEditorState.setHtml(A_REPLY) - val withMessageState = awaitItem() - assertThat(withMessageState.richTextEditorState.messageHtml).isEqualTo(A_REPLY) - assertThat(withMessageState.canSendMessage).isTrue() - withMessageState.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) - skipItems(1) + assertThat(state.richTextEditorState.messageHtml).isEqualTo(A_REPLY) + state.eventSink.invoke(MessageComposerEvents.SendMessage(A_REPLY.toMessage())) val messageSentState = awaitItem() assertThat(messageSentState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(messageSentState.canSendMessage).isFalse() assertThat(fakeMatrixRoom.replyMessageParameter).isEqualTo(A_REPLY to A_REPLY) assertThat(analyticsService.capturedEvents).containsExactly( Composer( @@ -703,7 +689,6 @@ class MessageComposerPresenterTest { val normalState = awaitItem() assertThat(normalState.mode).isEqualTo(MessageComposerMode.Normal("")) assertThat(normalState.richTextEditorState.messageHtml).isEqualTo("") - assertThat(normalState.canSendMessage).isFalse() } private fun createPresenter( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 209b92bbda..ab860468cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ dependencyanalysis = "1.22.0" stem = "2.3.0" sqldelight = "1.5.5" telephoto = "0.6.2" -wysiwyg = "2.12.0" +wysiwyg = "2.14.0" # DI dagger = "2.48" diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt new file mode 100644 index 0000000000..2400f2d7ee --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt @@ -0,0 +1,73 @@ +/* + * 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.designsystem.theme.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +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.RectangleShape +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun BottomSheetDragHandle() { + Box( + modifier = Modifier + .height(36.dp) + .background(Color.Transparent) + .fillMaxWidth() + .clip(RectangleShape), + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .requiredHeight(72.dp) + .offset(y = 18.dp) + .clip(MaterialTheme.shapes.extraLarge) + .background(MaterialTheme.colorScheme.background) + .border(0.5.dp, ElementTheme.colors.borderDisabled, MaterialTheme.shapes.extraLarge) + ) + + Box( + modifier = Modifier + .width(32.dp) + .height(4.dp) + .background(ElementTheme.colors.iconQuaternary, RoundedCornerShape(2.dp)) + ) + } +} + + +@PreviewsDayNight +@Composable +fun BottomSheetDragHandlePreview() { + BottomSheetDragHandle() +} diff --git a/libraries/textcomposer/impl/build.gradle.kts b/libraries/textcomposer/impl/build.gradle.kts index 86e911ca3e..1b96fec0e6 100644 --- a/libraries/textcomposer/impl/build.gradle.kts +++ b/libraries/textcomposer/impl/build.gradle.kts @@ -32,8 +32,6 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.testtags) - 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 4dac15d1b4..1cd1f17e3d 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 @@ -16,8 +16,6 @@ package io.element.android.libraries.textcomposer -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -26,13 +24,14 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize 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.requiredHeightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -41,10 +40,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -56,9 +55,6 @@ 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.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.text.applyScaleUp @@ -76,12 +72,14 @@ import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.textcomposer.components.FormattingOption import io.element.android.libraries.textcomposer.components.FormattingOptionState +import io.element.android.libraries.textcomposer.components.textInputRoundedCornerShape 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.RichTextEditorState import io.element.android.wysiwyg.view.models.InlineFormat import io.element.android.wysiwyg.view.models.LinkAction +import kotlinx.coroutines.launch import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction @@ -89,10 +87,10 @@ import uniffi.wysiwyg_composer.ComposerAction fun TextComposer( state: RichTextEditorState, composerMode: MessageComposerMode, - canSendMessage: Boolean, enableTextFormatting: Boolean, modifier: Modifier = Modifier, showTextFormatting: Boolean = false, + subcomposing: Boolean = false, onRequestFocus: () -> Unit = {}, onSendMessage: (Message) -> Unit = {}, onResetComposerMode: () -> Unit = {}, @@ -105,176 +103,246 @@ fun TextComposer( onSendMessage(Message(html = html, markdown = state.messageMarkdown)) } + val layoutModifier = modifier + .fillMaxSize() + .height(IntrinsicSize.Min) + + val composerOptionsButton = @Composable { + ComposerOptionsButton( + modifier = Modifier + .size(48.dp), + onClick = onAddAttachment + ) + } + + val textInput = @Composable { + TextInput( + state = state, + subcomposing = subcomposing, + placeholder = if (composerMode.inThread) { + stringResource(id = CommonStrings.action_reply_in_thread) + } else { + stringResource(id = R.string.rich_text_editor_composer_placeholder) + }, + composerMode = composerMode, + onResetComposerMode = onResetComposerMode, + onError = onError, + ) + } + + val sendButton = @Composable { + SendButton( + canSendMessage = state.messageHtml.isNotEmpty(), + onClick = onSendClicked, + composerMode = composerMode, + ) + } + + val textFormattingOptions = @Composable { TextFormatting(state = state) } + + if (showTextFormatting) { + TextFormattingLayout( + modifier = layoutModifier, + textInput = textInput, + dismissTextFormattingButton = { + DismissTextFormattingButton(onClick = onDismissTextFormatting) + }, + textFormatting = textFormattingOptions, + sendButton = sendButton + ) + } else { + StandardLayout( + modifier = layoutModifier, + composerOptionsButton = composerOptionsButton, + textInput = textInput, + sendButton = sendButton + ) + } + + if (!subcomposing) { + SoftKeyboardEffect(composerMode, onRequestFocus) { + it is MessageComposerMode.Special + } + + SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } + } +} + +@Composable +private fun StandardLayout( + textInput: @Composable () -> Unit, + composerOptionsButton: @Composable () -> Unit, + sendButton: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.Bottom, + ) { + 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 + .padding(bottom = 5.dp, top = 5.dp, end = 6.dp, start = 6.dp) + ) { + sendButton() + } + } +} + +@Composable +private fun TextFormattingLayout( + textInput: @Composable () -> Unit, + dismissTextFormattingButton: @Composable () -> Unit, + textFormatting: @Composable () -> Unit, + sendButton: @Composable () -> Unit, + modifier: Modifier = Modifier +) { Column( - modifier = modifier - .padding( - start = 3.dp, - end = 6.dp, - top = 8.dp, - bottom = 4.dp, - ) - .fillMaxWidth(), + modifier = modifier.padding(vertical = 4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - ConstraintLayout( - modifier = Modifier.fillMaxWidth(), + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 12.dp) ) { - val (composeOptions, textInput, sendButton) = createRefs() - val showComposerOptionsButton by remember(showTextFormatting) { - derivedStateOf { !showTextFormatting } - } - 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 + textInput() + } + Row( + modifier = Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Box( + modifier = Modifier.padding(start = 3.dp) ) { - Icon( - modifier = Modifier.size(30.dp.applyScaleUp()), - resourceId = CommonDrawables.ic_plus, - contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), - tint = ElementTheme.colors.iconPrimary, - ) + dismissTextFormattingButton() } - val roundCornerSmall = 20.dp.applyScaleUp() - val roundCornerLarge = 28.dp.applyScaleUp() - - val roundedCornerSize = remember(state.lineCount, composerMode) { - if (composerMode is MessageComposerMode.Special) { - roundCornerSmall - } else { - roundCornerLarge - } + Box(modifier = Modifier.weight(1f)) { + textFormatting() } - val roundedCornerSizeState = animateDpAsState( - targetValue = roundedCornerSize, - animationSpec = tween( - durationMillis = 100, - ), - label = "roundedCornerSizeAnimation" - ) - val roundedCorners = RoundedCornerShape(roundedCornerSizeState.value) - val colors = ElementTheme.colors - val bgColor = colors.bgSubtleSecondary - val borderColor = colors.borderDisabled - - 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(0.5.dp, borderColor, roundedCorners) - ) { - if (composerMode is MessageComposerMode.Special) { - ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) - } - TextInput( - state = state, - placeholder = if (composerMode.inThread) { - stringResource(id = CommonStrings.action_reply_in_thread) - } else { - stringResource(id = R.string.rich_text_editor_composer_placeholder) - }, - roundedCorners = roundedCorners, - bgColor = bgColor, - onError = onError, + Box( + modifier = Modifier.padding( + start = 14.dp, + end = 6.dp ) + ) { + sendButton() } - - 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 - ) - }, - ) } } - - SoftKeyboardEffect(composerMode, onRequestFocus) { - it is MessageComposerMode.Special - } - - SoftKeyboardEffect(showTextFormatting, onRequestFocus) { it } } @Composable private fun TextInput( state: RichTextEditorState, + subcomposing: Boolean, placeholder: String, - roundedCorners: RoundedCornerShape, - bgColor: Color, + composerMode: MessageComposerMode, + onResetComposerMode: () -> Unit, modifier: Modifier = Modifier, onError: (Throwable) -> Unit = {}, ) { - val minHeight = 42.dp.applyScaleUp() - val defaultTypography = ElementTheme.typography.fontBodyLgRegular - Box( + val bgColor = ElementTheme.colors.bgSubtleSecondary + val borderColor = ElementTheme.colors.borderDisabled + val roundedCorners = textInputRoundedCornerShape(composerMode = composerMode) + + Column( modifier = modifier - .heightIn(min = minHeight) - .background(color = bgColor, shape = roundedCorners) - .padding( - PaddingValues( + .clip(roundedCorners) + .border(0.5.dp, borderColor, roundedCorners) + .background(color = bgColor) + .requiredHeightIn(min = 42.dp.applyScaleUp()) + .fillMaxSize(), + ) { + if (composerMode is MessageComposerMode.Special) { + ComposerModeView(composerMode = composerMode, onResetComposerMode = onResetComposerMode) + } + val defaultTypography = ElementTheme.typography.fontBodyLgRegular + Box( + modifier = modifier + .padding( top = 4.dp.applyScaleUp(), bottom = 4.dp.applyScaleUp(), start = 12.dp.applyScaleUp(), end = 42.dp.applyScaleUp() ) - ) - .testTag(TestTags.richTextEditor), - contentAlignment = Alignment.CenterStart, - ) { + .testTag(TestTags.richTextEditor), + contentAlignment = Alignment.CenterStart, + ) { + // Placeholder + if (state.messageHtml.isEmpty()) { + Text( + placeholder, + style = defaultTypography.copy( + color = ElementTheme.colors.textSecondary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } - // Placeholder - if (state.messageHtml.isEmpty()) { - Text( - placeholder, - style = defaultTypography.copy( - color = ElementTheme.colors.textSecondary, + RichTextEditor( + state = state, + registerStateUpdates = !subcomposing, + modifier = Modifier + .padding(top = 6.dp, bottom = 6.dp) + .fillMaxWidth(), + style = ElementRichTextEditorStyle.create( + hasFocus = state.hasFocus ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, + onError = onError ) } + } +} - RichTextEditor( - state = state, - modifier = Modifier - .padding(top = 6.dp, bottom = 6.dp) - .fillMaxWidth(), - style = ElementRichTextEditorStyle.create( - hasFocus = state.hasFocus - ), - onError = onError +@Composable +private fun ComposerOptionsButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = CommonDrawables.ic_plus, + contentDescription = stringResource(R.string.rich_text_editor_a11y_add_attachment), + tint = ElementTheme.colors.iconPrimary, + ) + } +} + +@Composable +private fun DismissTextFormattingButton( + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + IconButton( + modifier = modifier + .size(48.dp), + onClick = onClick + ) { + Icon( + modifier = Modifier.size(30.dp.applyScaleUp()), + resourceId = CommonDrawables.ic_cancel, + contentDescription = stringResource(CommonStrings.action_close), + tint = ElementTheme.colors.iconPrimary, ) } } @@ -282,142 +350,157 @@ private fun TextInput( @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 = CommonDrawables.ic_cancel, - contentDescription = stringResource(CommonStrings.action_close), - tint = ElementTheme.colors.iconPrimary, - ) + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + + fun onInlineFormatClick(inlineFormat: InlineFormat) { + coroutineScope.launch { + state.toggleInlineFormat(inlineFormat) } + } - val scrollState = rememberScrollState() - Row( - modifier = Modifier - .constrainAs(formatting) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(close.end, margin = 1.dp) - end.linkTo(send.start, margin = 14.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(CommonDrawables.ic_bold), - contentDescription = stringResource(R.string.rich_text_editor_format_bold) - ) - FormattingOption( - state = state.actions[ComposerAction.ITALIC].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.Italic) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_italic), - contentDescription = stringResource(R.string.rich_text_editor_format_italic) - ) - FormattingOption( - state = state.actions[ComposerAction.UNDERLINE].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.Underline) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_underline), - contentDescription = stringResource(R.string.rich_text_editor_format_underline) - ) - FormattingOption( - state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.StrikeThrough) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_strikethrough), - contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) - ) + fun onToggleListClick(ordered: Boolean) { + coroutineScope.launch { + state.toggleList(ordered) + } + } - var linkDialogAction by remember { mutableStateOf(null) } + fun onIndentClick() { + coroutineScope.launch { + state.indent() + } + } - linkDialogAction?.let { - TextComposerLinkDialog( - onDismissRequest = { linkDialogAction = null }, - onCreateLinkRequest = state::insertLink, - onSaveLinkRequest = state::setLink, - onRemoveLinkRequest = state::removeLink, - linkAction = it, - ) - } + fun onUnindentClick() { + coroutineScope.launch { + state.unindent() + } + } - FormattingOption( - state = state.actions[ComposerAction.LINK].toButtonState(), - onClick = { linkDialogAction = state.linkAction }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_link), - contentDescription = stringResource(R.string.rich_text_editor_link) - ) + fun onCodeBlockClick() { + coroutineScope.launch { + state.toggleCodeBlock() + } + } - FormattingOption( - state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), - onClick = { state.toggleList(ordered = false) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_bullet_list), - contentDescription = stringResource(R.string.rich_text_editor_bullet_list) - ) - FormattingOption( - state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), - onClick = { state.toggleList(ordered = true) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_numbered_list), - contentDescription = stringResource(R.string.rich_text_editor_numbered_list) - ) - FormattingOption( - state = state.actions[ComposerAction.INDENT].toButtonState(), - onClick = { state.indent() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_increase), - contentDescription = stringResource(R.string.rich_text_editor_indent) - ) - FormattingOption( - state = state.actions[ComposerAction.UNINDENT].toButtonState(), - onClick = { state.unindent() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_decrease), - contentDescription = stringResource(R.string.rich_text_editor_unindent) - ) - FormattingOption( - state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), - onClick = { state.toggleInlineFormat(InlineFormat.InlineCode) }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_inline_code), - contentDescription = stringResource(R.string.rich_text_editor_inline_code) - ) - FormattingOption( - state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), - onClick = { state.toggleCodeBlock() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_code_block), - contentDescription = stringResource(R.string.rich_text_editor_code_block) - ) - FormattingOption( - state = state.actions[ComposerAction.QUOTE].toButtonState(), - onClick = { state.toggleQuote() }, - imageVector = ImageVector.vectorResource(CommonDrawables.ic_quote), - contentDescription = stringResource(R.string.rich_text_editor_quote) + fun onQuoteClick() { + coroutineScope.launch { + state.toggleQuote() + } + } + + fun onCreateLinkRequest(url: String, text: String) { + coroutineScope.launch { + state.insertLink(url, text) + } + } + + fun onSaveLinkRequest(url: String) { + coroutineScope.launch { + state.setLink(url) + } + } + + fun onRemoveLinkRequest() { + coroutineScope.launch { + state.removeLink() + } + } + + Row( + modifier = modifier + .horizontalScroll(scrollState), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + FormattingOption( + state = state.actions[ComposerAction.BOLD].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Bold) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_bold), + contentDescription = stringResource(R.string.rich_text_editor_format_bold) + ) + FormattingOption( + state = state.actions[ComposerAction.ITALIC].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Italic) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_italic), + contentDescription = stringResource(R.string.rich_text_editor_format_italic) + ) + FormattingOption( + state = state.actions[ComposerAction.UNDERLINE].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.Underline) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_underline), + contentDescription = stringResource(R.string.rich_text_editor_format_underline) + ) + FormattingOption( + state = state.actions[ComposerAction.STRIKE_THROUGH].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.StrikeThrough) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_strikethrough), + contentDescription = stringResource(R.string.rich_text_editor_format_strikethrough) + ) + + var linkDialogAction by remember { mutableStateOf(null) } + + linkDialogAction?.let { + TextComposerLinkDialog( + onDismissRequest = { linkDialogAction = null }, + onCreateLinkRequest = ::onCreateLinkRequest, + onSaveLinkRequest = ::onSaveLinkRequest, + onRemoveLinkRequest = ::onRemoveLinkRequest, + linkAction = it, ) } - sendButton( - Modifier.constrainAs(send) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - end.linkTo(parent.end) - }, + FormattingOption( + state = state.actions[ComposerAction.LINK].toButtonState(), + onClick = { linkDialogAction = state.linkAction }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_link), + contentDescription = stringResource(R.string.rich_text_editor_link) + ) + + FormattingOption( + state = state.actions[ComposerAction.UNORDERED_LIST].toButtonState(), + onClick = { onToggleListClick(ordered = false) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_bullet_list), + contentDescription = stringResource(R.string.rich_text_editor_bullet_list) + ) + FormattingOption( + state = state.actions[ComposerAction.ORDERED_LIST].toButtonState(), + onClick = { onToggleListClick(ordered = true) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_numbered_list), + contentDescription = stringResource(R.string.rich_text_editor_numbered_list) + ) + FormattingOption( + state = state.actions[ComposerAction.INDENT].toButtonState(), + onClick = { onIndentClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_increase), + contentDescription = stringResource(R.string.rich_text_editor_indent) + ) + FormattingOption( + state = state.actions[ComposerAction.UNINDENT].toButtonState(), + onClick = { onUnindentClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_indent_decrease), + contentDescription = stringResource(R.string.rich_text_editor_unindent) + ) + FormattingOption( + state = state.actions[ComposerAction.INLINE_CODE].toButtonState(), + onClick = { onInlineFormatClick(InlineFormat.InlineCode) }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_inline_code), + contentDescription = stringResource(R.string.rich_text_editor_inline_code) + ) + FormattingOption( + state = state.actions[ComposerAction.CODE_BLOCK].toButtonState(), + onClick = { onCodeBlockClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_code_block), + contentDescription = stringResource(R.string.rich_text_editor_code_block) + ) + FormattingOption( + state = state.actions[ComposerAction.QUOTE].toButtonState(), + onClick = { onQuoteClick() }, + imageVector = ImageVector.vectorResource(CommonDrawables.ic_quote), + contentDescription = stringResource(R.string.rich_text_editor_quote) ) } } @@ -614,95 +697,93 @@ private fun SendButton( @PreviewsDayNight @Composable internal fun TextComposerSimplePreview() = ElementPreview { - Column { - TextComposer( - RichTextEditorState("", fake = true).apply { requestFocus() }, - canSendMessage = false, - onSendMessage = {}, - composerMode = MessageComposerMode.Normal(""), - onResetComposerMode = {}, - enableTextFormatting = true, - ) + PreviewColumn(items = listOf( + { + TextComposer( + RichTextEditorState("", initialFocus = true), + onSendMessage = {}, + composerMode = MessageComposerMode.Normal(""), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + }, { TextComposer( - RichTextEditorState("A message", fake = true).apply { requestFocus() }, - canSendMessage = true, + RichTextEditorState("A message", initialFocus = true), onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( RichTextEditorState( "A message\nWith several lines\nTo preview larger textfields and long lines with overflow", - fake = true - ).apply { - requestFocus() - }, - canSendMessage = true, + initialFocus = true + ), onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message without focus", fake = true), - canSendMessage = true, + RichTextEditorState("A message without focus", initialFocus = false), onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, enableTextFormatting = true, ) - } + }) + ) } @PreviewsDayNight @Composable internal fun TextComposerFormattingPreview() = ElementPreview { - Column { + PreviewColumn(items = listOf({ TextComposer( - RichTextEditorState("", fake = true), - canSendMessage = false, + RichTextEditorState("", initialFocus = false), showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message", initialFocus = false), showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", fake = true), - canSendMessage = true, + RichTextEditorState("A message\nWith several lines\nTo preview larger textfields and long lines with overflow", initialFocus = false), showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), enableTextFormatting = true, ) - } + })) } @PreviewsDayNight @Composable internal fun TextComposerEditPreview() = ElementPreview { - TextComposer( - RichTextEditorState("A message", fake = true).apply { requestFocus() }, - canSendMessage = true, - onSendMessage = {}, - composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), - onResetComposerMode = {}, - enableTextFormatting = true, - ) + PreviewColumn(items = listOf { + TextComposer( + RichTextEditorState("A message", initialFocus = true), + onSendMessage = {}, + composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + }) } @PreviewsDayNight @Composable internal fun TextComposerReplyPreview() = ElementPreview { - Column { + PreviewColumn(items = listOf({ TextComposer( - RichTextEditorState("", fake = true), - canSendMessage = false, + RichTextEditorState(""), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -716,25 +797,26 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, + { + TextComposer( + RichTextEditorState(""), + onSendMessage = {}, + composerMode = MessageComposerMode.Reply( + isThreaded = true, + senderName = "Alice", + eventId = EventId("$1234"), + attachmentThumbnailInfo = null, + defaultContent = "A message\n" + + "With several lines\n" + + "To preview larger textfields and long lines with overflow" + ), + onResetComposerMode = {}, + enableTextFormatting = true, + ) + }, { TextComposer( - RichTextEditorState("", fake = true), - canSendMessage = false, - onSendMessage = {}, - composerMode = MessageComposerMode.Reply( - isThreaded = true, - senderName = "Alice", - eventId = EventId("$1234"), - attachmentThumbnailInfo = null, - defaultContent = "A message\n" + - "With several lines\n" + - "To preview larger textfields and long lines with overflow" - ), - onResetComposerMode = {}, - enableTextFormatting = true, - ) - TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message"), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = true, @@ -751,9 +833,9 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message"), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -770,9 +852,9 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true), - canSendMessage = true, + RichTextEditorState("A message"), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -789,9 +871,9 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }, { TextComposer( - RichTextEditorState("A message", fake = true).apply { requestFocus() }, - canSendMessage = true, + RichTextEditorState("A message", initialFocus = true), onSendMessage = {}, composerMode = MessageComposerMode.Reply( isThreaded = false, @@ -808,5 +890,24 @@ internal fun TextComposerReplyPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) + }) + ) +} + +@Composable +private fun PreviewColumn( + modifier: Modifier = Modifier, + items: List<@Composable () -> Unit>, +) { + Column( + modifier = modifier + ) { + items.forEach { item -> + Box( + modifier = Modifier.height(IntrinsicSize.Min) + ) { + item() + } + } } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt new file mode 100644 index 0000000000..cf975634f6 --- /dev/null +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/TextInputRoundedCornerShape.kt @@ -0,0 +1,48 @@ +/* + * 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.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.text.applyScaleUp +import io.element.android.libraries.textcomposer.MessageComposerMode + +@Composable +internal fun textInputRoundedCornerShape( + composerMode: MessageComposerMode, +): RoundedCornerShape { + val roundCornerSmall = 20.dp.applyScaleUp() + val roundCornerLarge = 21.dp.applyScaleUp() + + val roundedCornerSize = if (composerMode is MessageComposerMode.Special) { + roundCornerSmall + } else { + roundCornerLarge + } + + val roundedCornerSizeState = animateDpAsState( + targetValue = roundedCornerSize, + animationSpec = tween( + durationMillis = 100, + ), + label = "roundedCornerSizeAnimation" + ) + return RoundedCornerShape(roundedCornerSizeState.value) +} From 215251127c3db338fb9f44550b6c19c8d47467be Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Tue, 26 Sep 2023 16:34:19 +0100 Subject: [PATCH 2/7] Fix scrolling when content overflows --- .../impl/ExpandableBottomSheetScaffold.kt | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt index 884895e2bb..80ec0585b6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt @@ -28,8 +28,12 @@ import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable @@ -47,7 +51,7 @@ import kotlin.math.roundToInt * @param content The main content. * @param sheetContent The sheet content. * @param sheetDragHandle The drag handle for the sheet. - * @param sheetSwipeEnabled Whether the sheet can be swiped. + * @param sheetSwipeEnabled Whether the sheet can be swiped. This value is ignored and swipe is disabled if the sheet content overflows. * @param sheetShape The shape of the sheet. * @param sheetTonalElevation The tonal elevation of the sheet. * @param sheetShadowElevation The shadow elevation of the sheet. @@ -72,8 +76,18 @@ internal fun ExpandableBottomSheetScaffold( skipHiddenState = true, ) ) - LaunchedEffect(sheetSwipeEnabled) { - if (!sheetSwipeEnabled) { + + // If the content overflows, we disable swipe to prevent the sheet from intercepting + // scroll events of the sheet content. + var contentOverflows by remember { mutableStateOf(false) } + val sheetSwipeEnabledIfPossible by remember(contentOverflows, sheetSwipeEnabled) { + derivedStateOf { + sheetSwipeEnabled && !contentOverflows + } + } + + LaunchedEffect(sheetSwipeEnabledIfPossible) { + if (!sheetSwipeEnabledIfPossible) { scaffoldState.bottomSheetState.partialExpand() } } @@ -83,7 +97,7 @@ internal fun ExpandableBottomSheetScaffold( modifier = Modifier, scaffoldState = scaffoldState, sheetPeekHeight = peekHeight, - sheetSwipeEnabled = sheetSwipeEnabled, + sheetSwipeEnabled = sheetSwipeEnabledIfPossible, sheetDragHandle = dragHandle, sheetShape = sheetShape, content = content, @@ -103,9 +117,14 @@ internal fun ExpandableBottomSheetScaffold( }.firstOrNull() val dragHandleHeight = dragHandleSub?.height?.toDp() ?: 0.dp + val maxHeight = constraints.maxHeight.toDp() + val contentHeight = sheetContentSub.height.toDp() + dragHandleHeight + + contentOverflows = contentHeight > maxHeight + val peekHeight = min( - constraints.maxHeight.toDp(), // prevent the sheet from expanding beyond the screen - sheetContentSub.height.toDp() + dragHandleHeight + maxHeight, // prevent the sheet from expanding beyond the screen + contentHeight ) val scaffoldPlaceables = subcompose(Slot.Scaffold) { From 8c0f258a3834b50fa1e8dfe26ffcb20b0848b7f5 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Tue, 26 Sep 2023 17:09:59 +0100 Subject: [PATCH 3/7] Fix lint issues --- .../impl/ExpandableBottomSheetScaffold.kt | 52 +++++++++++-------- .../features/messages/impl/MessagesView.kt | 10 ++-- .../theme/components/BottomSheetDragHandle.kt | 9 ++-- .../libraries/textcomposer/TextComposer.kt | 16 +++--- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt index 80ec0585b6..8b28a216b0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/ExpandableBottomSheetScaffold.kt @@ -91,25 +91,31 @@ internal fun ExpandableBottomSheetScaffold( scaffoldState.bottomSheetState.partialExpand() } } - val scaffold: @Composable (sheetContent: @Composable () -> Unit, dragHandle: @Composable () -> Unit, peekHeight: Dp) -> Unit = - { sheetContent, dragHandle, peekHeight -> - BottomSheetScaffold( - modifier = Modifier, - scaffoldState = scaffoldState, - sheetPeekHeight = peekHeight, - sheetSwipeEnabled = sheetSwipeEnabledIfPossible, - sheetDragHandle = dragHandle, - sheetShape = sheetShape, - content = content, - sheetContent = { sheetContent() }, - sheetTonalElevation = sheetTonalElevation, - sheetShadowElevation = sheetShadowElevation, - ) - } + + @Composable + fun Scaffold( + sheetContent: @Composable () -> Unit, + dragHandle: @Composable () -> Unit, + peekHeight: Dp, + ) { + BottomSheetScaffold( + modifier = Modifier, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetSwipeEnabled = sheetSwipeEnabledIfPossible, + sheetDragHandle = dragHandle, + sheetShape = sheetShape, + content = content, + sheetContent = { sheetContent() }, + sheetTonalElevation = sheetTonalElevation, + sheetShadowElevation = sheetShadowElevation, + ) + } + SubcomposeLayout( modifier = modifier, measurePolicy = { constraints: Constraints -> - val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(subcomposing = true) }.map { + val sheetContentSub = subcompose(Slot.SheetContent(sheetContentKey)) { sheetContent(true) }.map { it.measure(Constraints(maxWidth = constraints.maxWidth)) }.first() val dragHandleSub = subcompose(Slot.DragHandle) { sheetDragHandle() }.map { @@ -128,13 +134,13 @@ internal fun ExpandableBottomSheetScaffold( ) val scaffoldPlaceables = subcompose(Slot.Scaffold) { - scaffold({ + Scaffold({ Layout( modifier = Modifier.fillMaxHeight(), measurePolicy = { measurables, constraints -> - val maxHeight = constraints.maxHeight + val constraintHeight = constraints.maxHeight val offset = scaffoldState.bottomSheetState.getOffset() ?: 0 - val height = Integer.max(0, maxHeight - offset) + val height = Integer.max(0, constraintHeight - offset) val top = measurables[0].measure( constraints.copy( minHeight = height, @@ -145,7 +151,7 @@ internal fun ExpandableBottomSheetScaffold( top.place(x = 0, y = 0) } }, - content = { sheetContent(subcomposing = false) }) + content = { sheetContent(false) }) }, sheetDragHandle, peekHeight) }.map { measurable: Measurable -> measurable.measure(constraints) @@ -164,8 +170,8 @@ private fun SheetState.getOffset(): Int? = try { } private sealed class Slot { - data class SheetContent(val key: Int?): Slot() - data object DragHandle: Slot() - data object Scaffold: Slot() + data class SheetContent(val key: Int?) : Slot() + data object DragHandle : Slot() + data object Scaffold : Slot() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 6602b72267..88090f022c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -71,8 +71,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.BottomSheetDragHandle import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text @@ -284,9 +284,13 @@ private fun MessagesViewContent( .imePadding(), ) { ExpandableBottomSheetScaffold( - sheetDragHandle = if (state.composerState.showTextFormatting) ({ BottomSheetDragHandle() }) else ({}), + sheetDragHandle = if (state.composerState.showTextFormatting) { + @Composable { BottomSheetDragHandle() } + } else { + @Composable {} + }, sheetSwipeEnabled = state.composerState.showTextFormatting, - sheetShape = if(state.composerState.showTextFormatting) MaterialTheme.shapes.large else RectangleShape, + sheetShape = if (state.composerState.showTextFormatting) MaterialTheme.shapes.large else RectangleShape, content = { paddingValues -> TimelineView( modifier = Modifier.padding(paddingValues), diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt index 2400f2d7ee..63dcf1b653 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/BottomSheetDragHandle.kt @@ -33,13 +33,16 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.theme.ElementTheme @Composable -fun BottomSheetDragHandle() { +fun BottomSheetDragHandle( + modifier: Modifier = Modifier +) { Box( - modifier = Modifier + modifier = modifier .height(36.dp) .background(Color.Transparent) .fillMaxWidth() @@ -68,6 +71,6 @@ fun BottomSheetDragHandle() { @PreviewsDayNight @Composable -fun BottomSheetDragHandlePreview() { +internal fun BottomSheetDragHandlePreview() = ElementPreview { BottomSheetDragHandle() } 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 1cd1f17e3d..72246b8f48 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 @@ -79,6 +79,8 @@ import io.element.android.wysiwyg.compose.RichTextEditor import io.element.android.wysiwyg.compose.RichTextEditorState import io.element.android.wysiwyg.view.models.InlineFormat import io.element.android.wysiwyg.view.models.LinkAction +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.launch import uniffi.wysiwyg_composer.ActionState import uniffi.wysiwyg_composer.ComposerAction @@ -272,7 +274,7 @@ private fun TextInput( } val defaultTypography = ElementTheme.typography.fontBodyLgRegular Box( - modifier = modifier + modifier = Modifier .padding( top = 4.dp.applyScaleUp(), bottom = 4.dp.applyScaleUp(), @@ -697,7 +699,7 @@ private fun SendButton( @PreviewsDayNight @Composable internal fun TextComposerSimplePreview() = ElementPreview { - PreviewColumn(items = listOf( + PreviewColumn(items = persistentListOf( { TextComposer( RichTextEditorState("", initialFocus = true), @@ -740,7 +742,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { @PreviewsDayNight @Composable internal fun TextComposerFormattingPreview() = ElementPreview { - PreviewColumn(items = listOf({ + PreviewColumn(items = persistentListOf({ TextComposer( RichTextEditorState("", initialFocus = false), showTextFormatting = true, @@ -767,7 +769,7 @@ internal fun TextComposerFormattingPreview() = ElementPreview { @PreviewsDayNight @Composable internal fun TextComposerEditPreview() = ElementPreview { - PreviewColumn(items = listOf { + PreviewColumn(items = persistentListOf({ TextComposer( RichTextEditorState("A message", initialFocus = true), onSendMessage = {}, @@ -775,13 +777,13 @@ internal fun TextComposerEditPreview() = ElementPreview { onResetComposerMode = {}, enableTextFormatting = true, ) - }) + })) } @PreviewsDayNight @Composable internal fun TextComposerReplyPreview() = ElementPreview { - PreviewColumn(items = listOf({ + PreviewColumn(items = persistentListOf({ TextComposer( RichTextEditorState(""), onSendMessage = {}, @@ -896,8 +898,8 @@ internal fun TextComposerReplyPreview() = ElementPreview { @Composable private fun PreviewColumn( + items: ImmutableList<@Composable () -> Unit>, modifier: Modifier = Modifier, - items: List<@Composable () -> Unit>, ) { Column( modifier = modifier From ac76393ee03493561ecd4d66533a20520d207a26 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 28 Sep 2023 13:46:23 +0100 Subject: [PATCH 4/7] Update previews --- .../features/messages/impl/MessagesStateProvider.kt | 2 +- .../impl/messagecomposer/MessageComposerView.kt | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 5a430c7286..5642bd4d2b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -39,13 +39,13 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState(), aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), - aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)), aMessagesState().copy(userHasPermissionToSendMessage = false), aMessagesState().copy(showReinvitePrompt = true), aMessagesState().copy( roomName = Async.Uninitialized, roomAvatar = Async.Uninitialized, ), + aMessagesState().copy(composerState = aMessageComposerState().copy(showTextFormatting = true)), ) } 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 e6be16013b..b2ab7d5da2 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 @@ -17,6 +17,8 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope @@ -92,8 +94,17 @@ fun MessageComposerView( @PreviewsDayNight @Composable internal fun MessageComposerViewPreview(@PreviewParameter(MessageComposerStateProvider::class) state: MessageComposerState) = ElementPreview { - Box(modifier = Modifier.height(200.dp)) { + Column { MessageComposerView( + modifier = Modifier.height(IntrinsicSize.Min), + state = state, + onSendLocationClicked = {}, + onCreatePollClicked = {}, + enableTextFormatting = true, + subcomposing = false, + ) + MessageComposerView( + modifier = Modifier.height(200.dp), state = state, onSendLocationClicked = {}, onCreatePollClicked = {}, From fb7d73b0e6c79662ada42ab2a08302d390507be3 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 28 Sep 2023 11:48:59 +0000 Subject: [PATCH 5/7] Update screenshots --- ..._null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png | 3 +++ ...es.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...es.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png | 3 +++ ...ts_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ts_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png | 3 +++ ...poser_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png | 4 ++-- ...poser_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png | 4 ++-- ...null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png | 4 ++-- ...oser_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png | 4 ++-- ...ser_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ser_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- 22 files changed, 48 insertions(+), 36 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png index ce9f3bd552..9c2309187b 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-D-4_4_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:365e3b0f92dd56b4404f50861d119fa58435e9d9c887142632f5566e0dc4c9f1 -size 10796 +oid sha256:b91884f47f7789ccb1e22b29be6cfcfa6fa5975614145eedd4d884b867cfcf0f +size 18458 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png index 21df3b6b35..e119b62da1 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl.messagecomposer_null_MessageComposerView-N-4_5_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7213c01580a7c0caa8d63a8540c9f69208ac8d906a70fd6399c4fa5cacb606e -size 10093 +oid sha256:0c0892fa19589139b4d47a514f0a56486ab47f82b94ef9426f8bb623be7cc0cb +size 16786 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png index d945b72faf..72056b5ed9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f78a54b9592de1a24bac3796f725e781595c32330f3d340d96c196c0e732bb9d -size 53831 +oid sha256:88bdef3999877e5017bfe0e0ead1514e4e6a58abcde0b0167d4b0ad9d4abd1e0 +size 54020 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png index f716e19da3..90b4f4652f 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e16e72290141082417b835a836c81f0a8b127412a04e8cb2c2a36ea8c922c2e6 -size 55147 +oid sha256:35f420b550029d7f8b22d73ea0349d2794cc2e5c5f3080799f496774fad7d2ff +size 55440 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png index e0733fd793..1ae57f1414 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b010513e52a549f498f1458db6ebdda6ecf4ad3f886a570a47142cbd8704f90 -size 55907 +oid sha256:3f62a8a4eded0b742e911970837fe1003228834dd07a216a9bdc4acef37aa468 +size 55800 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png index ff4fb0564f..31edf89ccf 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20eee7798706209e11f5d59ab816b7a0589481fb7c0059d2559c848cc992e8ef -size 51481 +oid sha256:1314aaf5394d03b5d08eccdb29783ce8d949f44d3eea28ec8ce434b830515304 +size 51662 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..617d8da89b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-D-0_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecdc26ae1b8943734a8ec1020d7ca9ad4a8e570f5295eb568f80ed314585a9a9 +size 51981 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png index fec3bbf05e..58b944edce 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54352c8066ebb4ac6c0079373cc967abcf9c99f04ddcfce0161fff3bfc34ad6c -size 52258 +oid sha256:32ae9c61f8a01bd54b9fb51af5f0dff222f4df0be1003a9c6ad680d20877448b +size 52275 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png index 77c73664d1..e28209550e 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:690ffd45d28f6056b835f21c495ff6d959e089d376e1afcb10395d2e8dd487f3 -size 53490 +oid sha256:1785f0fe49a5afd9b152f6ddb51acad7ba1700b2711cf302ef3a490d948fcd96 +size 53618 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png index 06610e4888..6162f39468 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:84f147d29e31f1d99519e282d69c0dfa24363fc167d2c6eda323590a70d63677 -size 51222 +oid sha256:455b414e5da5d5174a8d87ef37219c9754e3558b39f025d7aab226b041c51096 +size 51305 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png index f8ee215679..3b7c0855e3 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab95b8dd9c480e304333f67032ef42c7a66bc717572a328dbf2f9551773552f7 -size 49850 +oid sha256:a0b83b1d37b34cdf0769e83f625d66479638ce402d5c8e76ef40548414d34400 +size 49862 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..500b83a53e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[f.messages.impl_null_MessagesView-N-0_1_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b09d4a60c3d9944cd6d50a8f0a06d5b3522eba7884f3b5e1e6889f93e8dd1794 +size 50026 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..182cc3a083 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c5bd25caa8c462fd9a98fef14f011450adb414ec14d1a8b285d37692185442c +size 5616 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..86a758d3b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.designsystem.theme.components_null_BottomSheetDragHandle-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84d00d9738ffbd2b3798e3c31b13d4a086377abaf250049aca24fdfef1132ed8 +size 5459 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png index ce58d7b6ab..e6274d3ab9 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-D-2_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3a8bb9cbb98e7acf4d5e6aee29071ce851229622ada00723336f1167430f123 -size 13730 +oid sha256:a0c5c53f53eb3cdda73391fabda2658b5c009e9a13761ef9acae37530f3cb1b5 +size 14017 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png index 55268f535a..59f70b2c3c 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerEdit-N-2_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38e673e6cb326662096558fc9f25b5b84835004217ae06d2be75e9d6232e0ddf -size 12893 +oid sha256:e5b786b07d92459399099e0f7730804de2f2123d8c5eb51ca64f7609768ecc34 +size 13157 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png index 87893f6777..bfc1d62dbc 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-D-1_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:463156fde1b2766a5c17f0e4aaac79243496d832ac115d090ec212e0077c4b4a -size 41560 +oid sha256:54f6712d9cbd60c6a6d108ddcff08492a5e74cbba7d1f4b5f3312a068e4dd798 +size 43207 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png index 000ebf17b9..e30b7ac05a 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerFormatting-N-1_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f490d44a0609fac5f4ae08ec20472cf55ca90bbd7b4c719087b47be4d4e9ab14 -size 39270 +oid sha256:e156ed6831abb52d8bbd95934517f423d0cae06e21e9266b099174aad55228ab +size 40662 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png index 5e4d0a391d..d47caa7de0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-D-3_3_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16009c30863c10bda995eb570637d754075703d2bcce95b366e34667a5d53671 -size 84173 +oid sha256:fe441727d1a64a7439a15780a208b50bfacd875d6a6fa9bee980508e9beda5a7 +size 87262 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png index d602d87983..a42ece91ed 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerReply-N-3_4_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:179ee5b9d4d8df4a54c09e7406ec006b3bc7407ce27daa2d69369ca89cee373d -size 80573 +oid sha256:031e8632fb034425c6e30801abb5f57271e5411784501ee27cd413db363e2d67 +size 83036 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png index 360c35f3b1..a6f891a3a0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-D-0_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da51cd385e731b4544488be10e9a71f79714539d7cbcaaa0b760f3e2b3cf9272 -size 45199 +oid sha256:fd38c36b9b85c3ca3e290e2c0fd338ca52552398d2497a92a573ffd3f133b567 +size 47937 diff --git a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png index 0462ba7ca3..c3d1dc9ae0 100644 --- a/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/ui_S_t[l.textcomposer_null_TextComposerSimple-N-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1624403c6bbd12925b84551215c1ea3a2d3073375d3422ee1ffd5578a3635659 -size 42351 +oid sha256:033292295440c19363ebf1cc425c60226cf6f811a25219ae7939c8d2871091c4 +size 45020 From 5b702c1ff974d05f4403038b73dabfbb736af1b8 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Thu, 28 Sep 2023 14:45:04 +0100 Subject: [PATCH 6/7] Add changelog --- changelog.d/1447.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/1447.feature diff --git a/changelog.d/1447.feature b/changelog.d/1447.feature new file mode 100644 index 0000000000..af012c06cc --- /dev/null +++ b/changelog.d/1447.feature @@ -0,0 +1 @@ +[Rich text editor] Add full screen mode \ No newline at end of file From 2d31c8fe25c0844e7b103a53e2476f407f8489d3 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Fri, 29 Sep 2023 09:15:21 +0100 Subject: [PATCH 7/7] Add note about subcomposition --- .../io/element/android/libraries/textcomposer/TextComposer.kt | 2 ++ 1 file changed, 2 insertions(+) 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 72246b8f48..9d3db35959 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 @@ -298,6 +298,8 @@ private fun TextInput( RichTextEditor( state = state, + // Disable most of the editor functionality if it's just being measured for a subcomposition. + // This prevents it gaining focus and mutating the state. registerStateUpdates = !subcomposing, modifier = Modifier .padding(top = 6.dp, bottom = 6.dp)