Draft : refactor a bit ComposerMode and formatting management so we don't mess up with draft restoration.

This commit is contained in:
ganfra
2024-06-25 11:35:22 +02:00
parent 9f45005c05
commit dfae2e50c9
7 changed files with 87 additions and 72 deletions

View File

@@ -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 = ""
)
}

View File

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

View File

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

View File

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

View File

@@ -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")
)

View File

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

View File

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