[Rich text editor] Add feature flag for rich text editor (#1289)

This commit is contained in:
jonnyandrew
2023-09-13 09:17:02 +01:00
committed by GitHub
parent f136a308b2
commit d5b01ac0b4
16 changed files with 84 additions and 25 deletions

1
changelog.d/1289.feature Normal file
View File

@@ -0,0 +1 @@
[Rich text editor] Add feature flag for rich text editor. Markdown support can now be enabled by disabling the rich text editor.

View File

@@ -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<MessagesState> {
@@ -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,
)

View File

@@ -45,5 +45,6 @@ data class MessagesState(
val snackbarMessage: SnackbarMessage?,
val inviteProgress: Async<Unit>,
val showReinvitePrompt: Boolean,
val enableTextFormatting: Boolean,
val eventSink: (MessagesEvents) -> Unit
)

View File

@@ -82,5 +82,6 @@ fun aMessagesState() = MessagesState(
snackbarMessage = null,
inviteProgress = Async.Uninitialized,
showReinvitePrompt = false,
enableTextFormatting = true,
eventSink = {}
)

View File

@@ -304,6 +304,7 @@ private fun MessagesViewContent(
state = state.composerState,
onSendLocationClicked = onSendLocationClicked,
onCreatePollClicked = onCreatePollClicked,
enableTextFormatting = state.enableTextFormatting,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(Alignment.Bottom)

View File

@@ -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,
)
}

View File

@@ -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,
)
}

View File

@@ -628,6 +628,7 @@ class MessagesPresenterTest {
val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider())
val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom)
val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom)
val featureFlagsService = FakeFeatureFlagService(mapOf(FeatureFlags.RichTextEditor.key to true))
return MessagesPresenter(
room = matrixRoom,
composerPresenter = messageComposerPresenter,
@@ -642,6 +643,7 @@ class MessagesPresenterTest {
navigator = navigator,
clipboardHelper = clipboardHelper,
analyticsService = analyticsService,
featureFlagService = featureFlagsService,
dispatchers = coroutineDispatchers,
)
}

View File

@@ -46,7 +46,7 @@ dependencyanalysis = "1.21.0"
stem = "2.3.0"
sqldelight = "1.5.5"
telephoto = "0.6.0"
wysiwyg = "2.9.0"
wysiwyg = "2.10.0"
# DI
dagger = "2.48"

View File

@@ -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,
),
}

View File

@@ -35,6 +35,7 @@ class StaticFeatureFlagProvider @Inject constructor() :
FeatureFlags.LocationSharing -> true
FeatureFlags.Polls -> true
FeatureFlags.NotificationSettings -> false
FeatureFlags.RichTextEditor -> true
}
} else {
false

View File

@@ -79,11 +79,11 @@ interface MatrixRoom : Closeable {
suspend fun userAvatarUrl(userId: UserId): Result<String?>
suspend fun sendMessage(body: String, htmlBody: String): Result<Unit>
suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit>
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit>
suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit>
suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit>
suspend fun redactEvent(eventId: EventId, reason: String? = null): Result<Unit>

View File

@@ -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<Unit> = withContext(roomDispatcher) {
override suspend fun sendMessage(body: String, htmlBody: String?): Result<Unit> = 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<Unit> =
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> =
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<Unit> = withContext(roomDispatcher) {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> = 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)
}
}

View File

@@ -92,7 +92,7 @@ class FakeMatrixRoom(
private var sendPollResponseResult = Result.success(Unit)
private var endPollResult = Result.success(Unit)
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
val editMessageCalls = mutableListOf<Pair<String, String>>()
val editMessageCalls = mutableListOf<Pair<String, String?>>()
var sendMediaCount = 0
private set
@@ -171,7 +171,7 @@ class FakeMatrixRoom(
userAvatarUrlResult
}
override suspend fun sendMessage(body: String, htmlBody: String) = simulateLongTask {
override suspend fun sendMessage(body: String, htmlBody: String?) = simulateLongTask {
Result.success(Unit)
}
@@ -200,15 +200,15 @@ class FakeMatrixRoom(
return cancelSendResult
}
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String): Result<Unit> {
override suspend fun editMessage(originalEventId: EventId?, transactionId: TransactionId?, body: String, htmlBody: String?): Result<Unit> {
editMessageCalls += body to htmlBody
return Result.success(Unit)
}
var replyMessageParameter: Pair<String, String>? = null
var replyMessageParameter: Pair<String, String?>? = null
private set
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String): Result<Unit> {
override suspend fun replyMessage(eventId: EventId, body: String, htmlBody: String?): Result<Unit> {
replyMessageParameter = body to htmlBody
return Result.success(Unit)
}

View File

@@ -17,6 +17,6 @@
package io.element.android.libraries.textcomposer
data class Message(
val html: String,
val html: String?,
val markdown: String,
)

View File

@@ -91,6 +91,7 @@ fun TextComposer(
state: RichTextEditorState,
composerMode: MessageComposerMode,
canSendMessage: Boolean,
enableTextFormatting: Boolean,
modifier: Modifier = Modifier,
showTextFormatting: Boolean = false,
onRequestFocus: () -> Unit = {},
@@ -101,7 +102,8 @@ fun TextComposer(
onError: (Throwable) -> Unit = {},
) {
val onSendClicked = {
onSendMessage(Message(html = state.messageHtml, markdown = state.messageMarkdown))
val html = if (enableTextFormatting) state.messageHtml else null
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,
)
}
}