From afcdf246ebb38e970aabb4c5992d371b26a6a581 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Tue, 12 Sep 2023 17:13:08 +0100 Subject: [PATCH] Add feature flag for rich text editor --- .../messages/impl/MessagesPresenter.kt | 17 ++++++++++++-- .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 1 + .../features/messages/impl/MessagesView.kt | 1 + .../messagecomposer/AttachmentsBottomSheet.kt | 16 +++++++++---- .../messagecomposer/MessageComposerView.kt | 4 ++++ .../libraries/featureflag/api/FeatureFlags.kt | 5 ++++ .../impl/StaticFeatureFlagProvider.kt | 1 + .../libraries/matrix/api/room/MatrixRoom.kt | 6 ++--- .../matrix/impl/room/RustMatrixRoom.kt | 23 +++++++++++++------ .../android/libraries/textcomposer/Message.kt | 2 +- .../libraries/textcomposer/TextComposer.kt | 17 +++++++++++++- 12 files changed, 75 insertions(+), 19 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 0831afb6991..50b3dca2d1d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -66,6 +66,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -95,6 +97,7 @@ class MessagesPresenter @AssistedInject constructor( private val dispatchers: CoroutineDispatchers, private val clipboardHelper: ClipboardHelper, private val analyticsService: AnalyticsService, + private val featureFlagService: FeatureFlagService, @Assisted private val navigator: MessagesNavigator, ) : Presenter { @@ -143,6 +146,11 @@ class MessagesPresenter @AssistedInject constructor( timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId)) } + var enableTextFormatting by remember { mutableStateOf(true) } + LaunchedEffect(Unit) { + enableTextFormatting = featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor) + } + fun handleEvents(event: MessagesEvents) { when (event) { is MessagesEvents.HandleAction -> { @@ -178,6 +186,7 @@ class MessagesPresenter @AssistedInject constructor( snackbarMessage = snackbarMessage, showReinvitePrompt = showReinvitePrompt, inviteProgress = inviteProgress.value, + enableTextFormatting = enableTextFormatting, eventSink = { handleEvents(it) } ) } @@ -250,11 +259,15 @@ class MessagesPresenter @AssistedInject constructor( } } - private fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { + private suspend fun handleActionEdit(targetEvent: TimelineItem.Event, composerState: MessageComposerState) { val composerMode = MessageComposerMode.Edit( targetEvent.eventId, (targetEvent.content as? TimelineItemTextBasedContent)?.let { - it.htmlBody ?: it.body + if (featureFlagService.isFeatureEnabled(FeatureFlags.RichTextEditor)) { + it.htmlBody ?: it.body + } else { + it.body + } }.orEmpty(), targetEvent.transactionId, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index d22d54e7f36..46aad1e1912 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -45,5 +45,6 @@ data class MessagesState( val snackbarMessage: SnackbarMessage?, val inviteProgress: Async, val showReinvitePrompt: Boolean, + val enableTextFormatting: Boolean, val eventSink: (MessagesEvents) -> Unit ) 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 6ca799dc847..a88ebcbcd86 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 @@ -82,5 +82,6 @@ fun aMessagesState() = MessagesState( snackbarMessage = null, inviteProgress = Async.Uninitialized, showReinvitePrompt = false, + enableTextFormatting = true, eventSink = {} ) 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 ab11ca05d7c..1b4b8f7e195 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 @@ -304,6 +304,7 @@ private fun MessagesViewContent( state = state.composerState, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, + enableTextFormatting = state.enableTextFormatting, modifier = Modifier .fillMaxWidth() .wrapContentHeight(Alignment.Bottom) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt index 38ef458bd94..83320183858 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -55,6 +55,7 @@ internal fun AttachmentsBottomSheet( state: MessageComposerState, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { val localView = LocalView.current @@ -87,6 +88,7 @@ internal fun AttachmentsBottomSheet( ) { AttachmentSourcePickerMenu( state = state, + enableTextFormatting = enableTextFormatting, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, ) @@ -100,6 +102,7 @@ internal fun AttachmentSourcePickerMenu( state: MessageComposerState, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { Column( @@ -146,11 +149,13 @@ internal fun AttachmentSourcePickerMenu( text = { Text(stringResource(R.string.screen_room_attachment_source_poll)) }, ) } - ListItem( - modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) }, - icon = { Icon(Icons.Default.FormatColorText, null) }, - text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) }, - ) + if (enableTextFormatting) { + ListItem( + modifier = Modifier.clickable { state.eventSink(MessageComposerEvents.ToggleTextFormatting(enabled = true)) }, + icon = { Icon(Icons.Default.FormatColorText, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_text_formatting)) }, + ) + } } } @@ -163,5 +168,6 @@ internal fun AttachmentSourcePickerMenuPreview() = ElementPreview { ), onSendLocationClicked = {}, onCreatePollClicked = {}, + enableTextFormatting = 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 4ea5a7cf764..3413dda1072 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 @@ -31,6 +31,7 @@ fun MessageComposerView( state: MessageComposerState, onSendLocationClicked: () -> Unit, onCreatePollClicked: () -> Unit, + enableTextFormatting: Boolean, modifier: Modifier = Modifier, ) { fun onFullscreenToggle() { @@ -62,6 +63,7 @@ fun MessageComposerView( state = state, onSendLocationClicked = onSendLocationClicked, onCreatePollClicked = onCreatePollClicked, + enableTextFormatting = enableTextFormatting, ) TextComposer( @@ -74,6 +76,7 @@ fun MessageComposerView( onResetComposerMode = ::onCloseSpecialMode, onAddAttachment = ::onAddAttachment, onDismissTextFormatting = ::onDismissTextFormatting, + enableTextFormatting = enableTextFormatting, onError = ::onError, ) } @@ -95,5 +98,6 @@ private fun ContentToPreview(state: MessageComposerState) { state = state, onSendLocationClicked = {}, onCreatePollClicked = {}, + enableTextFormatting = true, ) } diff --git a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt index c7744f486dd..0f27a94a2de 100644 --- a/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt +++ b/libraries/featureflag/api/src/main/kotlin/io/element/android/libraries/featureflag/api/FeatureFlags.kt @@ -44,4 +44,9 @@ enum class FeatureFlags( // Do not forget to edit StaticFeatureFlagProvider when enabling the feature. defaultValue = false, ), + RichTextEditor( + key = "feature.richtexteditor", + title = "Enable rich text editor", + defaultValue = true, + ), } diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt index 50efdb9fc39..c9b24f08e6a 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/StaticFeatureFlagProvider.kt @@ -35,6 +35,7 @@ class StaticFeatureFlagProvider @Inject constructor() : FeatureFlags.LocationSharing -> true FeatureFlags.Polls -> true FeatureFlags.NotificationSettings -> false + FeatureFlags.RichTextEditor -> true } } else { false diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 169381f25e1..142e86dcd97 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -79,11 +79,11 @@ interface MatrixRoom : Closeable { suspend fun userAvatarUrl(userId: UserId): Result - suspend fun sendMessage(body: String, htmlBody: String): Result + suspend fun sendMessage(body: String, htmlBody: String?): Result - suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result + suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result - suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result + suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result suspend fun redactEvent(eventId: EventId, reason: String? = null): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 9e75d10aad2..eb456588eea 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -63,10 +63,12 @@ import org.matrix.rustcomponents.sdk.RequiredState import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomMember +import org.matrix.rustcomponents.sdk.RoomMessageEventContentWithoutRelation import org.matrix.rustcomponents.sdk.RoomSubscription import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromHtml +import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown import timber.log.Timber import java.io.File @@ -227,32 +229,32 @@ class RustMatrixRoom( } } - override suspend fun sendMessage(body: String, htmlBody: String): Result = withContext(roomDispatcher) { + override suspend fun sendMessage(body: String, htmlBody: String?): Result = withContext(roomDispatcher) { val transactionId = genTransactionId() - messageEventContentFromHtml(body, htmlBody).use { content -> + messageEventContentFromParts(body, htmlBody).use { content -> runCatching { innerRoom.send(content, transactionId) } } } - override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result = + override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result = withContext(roomDispatcher) { if (originalEventId != null) { runCatching { - innerRoom.edit(messageEventContentFromHtml(body, htmlBody), originalEventId.value, transactionId?.value) + innerRoom.edit(messageEventContentFromParts(body, htmlBody), originalEventId.value, transactionId?.value) } } else { runCatching { transactionId?.let { cancelSend(it) } - innerRoom.send(messageEventContentFromHtml(body, htmlBody), genTransactionId()) + innerRoom.send(messageEventContentFromParts(body, htmlBody), genTransactionId()) } } } - override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result = withContext(roomDispatcher) { + override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result = withContext(roomDispatcher) { runCatching { - innerRoom.sendReply(messageEventContentFromHtml(body, htmlBody), eventId.value, genTransactionId()) + innerRoom.sendReply(messageEventContentFromParts(body, htmlBody), eventId.value, genTransactionId()) } } @@ -456,4 +458,11 @@ class RustMatrixRoom( MediaUploadHandlerImpl(files, handle()) } } + + private fun messageEventContentFromParts(body: String, htmlBody: String?): RoomMessageEventContentWithoutRelation = + if(htmlBody != null) { + messageEventContentFromHtml(body, htmlBody) + } else { + messageEventContentFromMarkdown(body) + } } diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt index 0f3a2134279..ebc066188a4 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/Message.kt @@ -17,6 +17,6 @@ package io.element.android.libraries.textcomposer data class Message( - val html: String, + val html: String?, val markdown: String, ) 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 d137af18bb3..369c4947e31 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 @@ -98,10 +98,12 @@ fun TextComposer( onResetComposerMode: () -> Unit = {}, onAddAttachment: () -> Unit = {}, onDismissTextFormatting: () -> Unit = {}, + enableTextFormatting: Boolean, onError: (Throwable) -> Unit = {}, ) { val onSendClicked = { - onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown)) + val html = if (enableTextFormatting) state.messageHtml else state.messageMarkdown + onSendMessage(Message(html = html, markdown = state.messageMarkdown)) } Column( @@ -600,6 +602,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message", fake = true).apply { requestFocus() }, @@ -607,6 +610,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState( @@ -619,6 +623,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message without focus", fake = true), @@ -626,6 +631,7 @@ internal fun TextComposerSimplePreview() = ElementPreview { onSendMessage = {}, composerMode = MessageComposerMode.Normal(""), onResetComposerMode = {}, + enableTextFormatting = true, ) } } @@ -639,18 +645,21 @@ internal fun TextComposerFormattingPreview() = ElementPreview { canSendMessage = false, showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message", fake = true), canSendMessage = true, 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, showTextFormatting = true, composerMode = MessageComposerMode.Normal(""), + enableTextFormatting = true, ) } } @@ -664,6 +673,7 @@ internal fun TextComposerEditPreview() = ElementPreview { onSendMessage = {}, composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), onResetComposerMode = {}, + enableTextFormatting = true, ) } @@ -684,6 +694,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { "To preview larger textfields and long lines with overflow" ), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message", fake = true), @@ -701,6 +712,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { defaultContent = "image.jpg" ), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message", fake = true), @@ -718,6 +730,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { defaultContent = "video.mp4" ), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message", fake = true), @@ -735,6 +748,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { defaultContent = "logs.txt" ), onResetComposerMode = {}, + enableTextFormatting = true, ) TextComposer( RichTextEditorState("A message", fake = true).apply { requestFocus() }, @@ -752,6 +766,7 @@ internal fun TextComposerReplyPreview() = ElementPreview { defaultContent = "Shared location" ), onResetComposerMode = {}, + enableTextFormatting = true, ) } }