Draft : refactor a bit ComposerMode and formatting management so we don't mess up with draft restoration.
This commit is contained in:
@@ -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 = ""
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user