diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt index ee12c0ee8c..93e15f7868 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/send/SendLocationPresenterTest.kt @@ -379,8 +379,8 @@ class SendLocationPresenterTest { fakeMessageComposerContext.apply { composerMode = MessageComposerMode.Edit( eventId = null, - content = "", - transactionId = null + transactionId = null, + content = "" ) } @@ -427,8 +427,8 @@ class SendLocationPresenterTest { fakeMessageComposerContext.apply { composerMode = MessageComposerMode.Edit( eventId = null, - content = "", - transactionId = null + transactionId = null, + content = "" ) } 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 4d7adaeba9..c53a4a7111 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 @@ -312,6 +312,7 @@ class MessagesPresenter @AssistedInject constructor( else -> { val composerMode = MessageComposerMode.Edit( targetEvent.eventId, + targetEvent.transactionId, (targetEvent.content as? TimelineItemTextBasedContent)?.let { if (enableTextFormatting) { it.htmlBody ?: it.body @@ -319,7 +320,6 @@ class MessagesPresenter @AssistedInject constructor( it.body } }.orEmpty(), - targetEvent.transactionId, ) composerState.eventSink( MessageComposerEvents.SetMode(composerMode) 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 8e469e756f..6f44ca1f82 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 @@ -68,6 +68,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider +import io.element.android.libraries.textcomposer.model.MarkdownTextEditorState import io.element.android.libraries.textcomposer.model.Message import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.libraries.textcomposer.model.Suggestion @@ -75,6 +76,7 @@ import io.element.android.libraries.textcomposer.model.TextEditorState import io.element.android.libraries.textcomposer.model.rememberMarkdownTextEditorState import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction +import io.element.android.wysiwyg.compose.RichTextEditorState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CancellationException @@ -133,8 +135,6 @@ class MessageComposerPresenter @Inject constructor( override fun present(): MessageComposerState { val localCoroutineScope = rememberCoroutineScope() - // Initially disabled so we don't set focus and text twice - var applyFormattingModeChanges by remember { mutableStateOf(false) } val richTextEditorState = richTextEditorStateFactory.remember() if (isTesting) { richTextEditorState.isReadyToProcessActions = true @@ -182,18 +182,6 @@ class MessageComposerPresenter @Inject constructor( val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true) - LaunchedEffect(messageComposerContext.composerMode) { - when (val modeValue = messageComposerContext.composerMode) { - is MessageComposerMode.Edit -> - if (showTextFormatting) { - richTextEditorState.setHtml(modeValue.content) - } else { - markdownTextEditorState.text.update(modeValue.content, true) - } - else -> Unit - } - } - LaunchedEffect(attachmentsState.value) { when (val attachmentStateValue = attachmentsState.value) { is AttachmentsState.Sending.Processing -> { @@ -267,25 +255,7 @@ class MessageComposerPresenter @Inject constructor( ) LaunchedEffect(Unit) { - loadDraft(textEditorState) - } - - LaunchedEffect(showTextFormatting) { - if (!applyFormattingModeChanges) { - applyFormattingModeChanges = true - return@LaunchedEffect - } - if (showTextFormatting) { - val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder) - richTextEditorState.setMarkdown(markdown) - richTextEditorState.requestFocus() - } else { - val markdown = richTextEditorState.messageMarkdown - markdownTextEditorState.text.update(markdown, true) - // Give some time for the focus of the previous editor to be cleared - delay(100) - markdownTextEditorState.requestFocusAction() - } + loadDraft(markdownTextEditorState, richTextEditorState) } val mentionSpanProvider = if (isTesting) { @@ -338,19 +308,7 @@ class MessageComposerPresenter @Inject constructor( attachmentState = attachmentsState, ) is MessageComposerEvents.SetMode -> { - messageComposerContext.composerMode = event.composerMode - when (event.composerMode) { - is MessageComposerMode.Reply -> event.composerMode.eventId - is MessageComposerMode.Edit -> event.composerMode.eventId - is MessageComposerMode.Normal -> null - is MessageComposerMode.Quote -> null - }.let { relatedEventId -> - appCoroutineScope.launch { - timelineController.invokeOnCurrentTimeline { - enterSpecialMode(relatedEventId) - } - } - } + localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState) } MessageComposerEvents.AddAttachment -> localCoroutineScope.launch { showAttachmentSourcePicker = true @@ -398,10 +356,7 @@ class MessageComposerPresenter @Inject constructor( } is MessageComposerEvents.ToggleTextFormatting -> { showAttachmentSourcePicker = false - showTextFormatting = event.enabled - if (showTextFormatting) { - analyticsService.captureInteraction(Interaction.Name.MobileRoomComposerFormattingEnabled) - } + localCoroutineScope.toggleTextFormatting(event.enabled, markdownTextEditorState, richTextEditorState) } is MessageComposerEvents.Error -> { analyticsService.trackError(event.error) @@ -497,7 +452,6 @@ class MessageComposerPresenter @Inject constructor( } } - is MessageComposerMode.Quote -> TODO() is MessageComposerMode.Reply -> { timelineController.invokeOnCurrentTimeline { replyMessage(capturedMode.eventId, message.markdown, message.html, mentions) @@ -596,25 +550,33 @@ class MessageComposerPresenter @Inject constructor( } private fun CoroutineScope.loadDraft( - textEditorState: TextEditorState, + markdownTextEditorState: MarkdownTextEditorState, + richTextEditorState: RichTextEditorState, ) = launch { val draft = draftService.loadDraft(room.roomId) ?: return@launch val htmlText = draft.htmlText val markdownText = draft.plainText - textEditorState.setMarkdown(markdownText) if (htmlText != null) { - textEditorState.setHtml(htmlText) showTextFormatting = true + richTextEditorState.setHtml(htmlText) + richTextEditorState.requestFocus() + } else { + showTextFormatting = false + markdownTextEditorState.text.update(markdownText, true) + markdownTextEditorState.requestFocusAction() } when (val draftType = draft.draftType) { ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal - is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit(draftType.eventId, markdownText, null) + is ComposerDraftType.Edit -> messageComposerContext.composerMode = MessageComposerMode.Edit( + eventId = draftType.eventId, + transactionId = null, + content = htmlText ?: markdownText + ) is ComposerDraftType.Reply -> { messageComposerContext.composerMode = MessageComposerMode.Reply(InReplyToDetails.Loading(draftType.eventId)) timelineController.invokeOnCurrentTimeline { val replyToDetails = loadReplyDetails(draftType.eventId).map(permalinkParser) - messageComposerContext.composerMode = MessageComposerMode.Reply(replyToDetails) - Unit + run { messageComposerContext.composerMode = MessageComposerMode.Reply(replyToDetails) } } } } @@ -631,7 +593,6 @@ class MessageComposerPresenter @Inject constructor( mode.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) } } is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId) - is MessageComposerMode.Quote -> null } if (draftType == null || markdown.isBlank()) { return@launch @@ -644,4 +605,58 @@ class MessageComposerPresenter @Inject constructor( draftService.saveDraft(room.roomId, composerDraft) } } + + private fun CoroutineScope.toggleTextFormatting( + enabled: Boolean, + markdownTextEditorState: MarkdownTextEditorState, + richTextEditorState: RichTextEditorState, + ) = launch { + showTextFormatting = enabled + if (showTextFormatting) { + val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder) + richTextEditorState.setMarkdown(markdown) + richTextEditorState.requestFocus() + analyticsService.captureInteraction(Interaction.Name.MobileRoomComposerFormattingEnabled) + } else { + val markdown = richTextEditorState.messageMarkdown + markdownTextEditorState.text.update(markdown, true) + // Give some time for the focus of the previous editor to be cleared + delay(100) + markdownTextEditorState.requestFocusAction() + } + } + + private fun CoroutineScope.setMode( + composerMode: MessageComposerMode, + markdownTextEditorState: MarkdownTextEditorState, + richTextEditorState: RichTextEditorState + ) = launch { + messageComposerContext.composerMode = composerMode + when (composerMode) { + is MessageComposerMode.Reply -> { + timelineController.invokeOnCurrentTimeline { + enterSpecialMode(composerMode.eventId) + } + } + is MessageComposerMode.Edit -> { + setText(composerMode.content, markdownTextEditorState, richTextEditorState) + timelineController.invokeOnCurrentTimeline { + enterSpecialMode(composerMode.eventId) + } + } + else -> Unit + } + } + + private suspend fun setText(content: String, markdownTextEditorState: MarkdownTextEditorState, richTextEditorState: RichTextEditorState) { + if (showTextFormatting) { + richTextEditorState.setHtml(content) + } else { + markdownTextEditorState.text.update(content, true) + } + } } + + + + diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt index d35356458e..b286b948ea 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt @@ -60,7 +60,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_3 import io.element.android.libraries.matrix.test.A_USER_ID_4 -import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser @@ -1083,7 +1082,7 @@ fun anEditMode( eventId: EventId? = AN_EVENT_ID, message: String = A_MESSAGE, transactionId: TransactionId? = null, -) = MessageComposerMode.Edit(eventId, message, transactionId) +) = MessageComposerMode.Edit(eventId, transactionId, message) fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID)) fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE) 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 b158359814..8fff1ca813 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 @@ -601,7 +601,7 @@ internal fun TextComposerEditPreview() = ElementPreview { ATextComposer( TextEditorState.Rich(aRichTextEditorState(initialText = "A message", initialFocus = true)), voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), + composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"), enableVoiceMessages = true, currentUserId = UserId("@alice:localhost") ) @@ -615,7 +615,7 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview { ATextComposer( TextEditorState.Markdown(aMarkdownTextEditorState(initialText = "A message", initialFocus = true)), voiceMessageState = VoiceMessageState.Idle, - composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")), + composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"), enableVoiceMessages = true, currentUserId = UserId("@alice:localhost") ) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt index 1825e25f6e..ddd2a37535 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/components/SendButton.kt @@ -86,7 +86,7 @@ internal fun SendButton( @Composable internal fun SendButtonPreview() = ElementPreview { val normalMode = MessageComposerMode.Normal - val editMode = MessageComposerMode.Edit(null, "", null) + val editMode = MessageComposerMode.Edit(null, null, "") Row { SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode) SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode) diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt index eae5ec8678..577e5c3a3e 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/MessageComposerMode.kt @@ -29,9 +29,11 @@ sealed interface MessageComposerMode { sealed interface Special : MessageComposerMode - data class Edit(val eventId: EventId?, val content: String, val transactionId: TransactionId?) : Special - - class Quote(val eventId: EventId, val content: String) : Special + data class Edit( + val eventId: EventId?, + val transactionId: TransactionId?, + val content: String + ) : Special class Reply( val replyToDetails: InReplyToDetails @@ -43,7 +45,6 @@ sealed interface MessageComposerMode { get() = when (this) { is Normal -> null is Edit -> eventId - is Quote -> eventId is Reply -> eventId }