Draft : refactor a bit ComposerMode and formatting management so we don't mess up with draft restoration.
This commit is contained in:
parent
9f45005c05
commit
dfae2e50c9
7 changed files with 87 additions and 72 deletions
|
|
@ -379,8 +379,8 @@ class SendLocationPresenterTest {
|
||||||
fakeMessageComposerContext.apply {
|
fakeMessageComposerContext.apply {
|
||||||
composerMode = MessageComposerMode.Edit(
|
composerMode = MessageComposerMode.Edit(
|
||||||
eventId = null,
|
eventId = null,
|
||||||
content = "",
|
transactionId = null,
|
||||||
transactionId = null
|
content = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,8 +427,8 @@ class SendLocationPresenterTest {
|
||||||
fakeMessageComposerContext.apply {
|
fakeMessageComposerContext.apply {
|
||||||
composerMode = MessageComposerMode.Edit(
|
composerMode = MessageComposerMode.Edit(
|
||||||
eventId = null,
|
eventId = null,
|
||||||
content = "",
|
transactionId = null,
|
||||||
transactionId = null
|
content = ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,7 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
else -> {
|
else -> {
|
||||||
val composerMode = MessageComposerMode.Edit(
|
val composerMode = MessageComposerMode.Edit(
|
||||||
targetEvent.eventId,
|
targetEvent.eventId,
|
||||||
|
targetEvent.transactionId,
|
||||||
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
(targetEvent.content as? TimelineItemTextBasedContent)?.let {
|
||||||
if (enableTextFormatting) {
|
if (enableTextFormatting) {
|
||||||
it.htmlBody ?: it.body
|
it.htmlBody ?: it.body
|
||||||
|
|
@ -319,7 +320,6 @@ class MessagesPresenter @AssistedInject constructor(
|
||||||
it.body
|
it.body
|
||||||
}
|
}
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
targetEvent.transactionId,
|
|
||||||
)
|
)
|
||||||
composerState.eventSink(
|
composerState.eventSink(
|
||||||
MessageComposerEvents.SetMode(composerMode)
|
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.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
import io.element.android.libraries.textcomposer.mentions.ResolvedMentionSuggestion
|
||||||
import io.element.android.libraries.textcomposer.mentions.rememberMentionSpanProvider
|
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.Message
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.libraries.textcomposer.model.Suggestion
|
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.libraries.textcomposer.model.rememberMarkdownTextEditorState
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
|
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.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
@ -133,8 +135,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
override fun present(): MessageComposerState {
|
override fun present(): MessageComposerState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
// Initially disabled so we don't set focus and text twice
|
|
||||||
var applyFormattingModeChanges by remember { mutableStateOf(false) }
|
|
||||||
val richTextEditorState = richTextEditorStateFactory.remember()
|
val richTextEditorState = richTextEditorStateFactory.remember()
|
||||||
if (isTesting) {
|
if (isTesting) {
|
||||||
richTextEditorState.isReadyToProcessActions = true
|
richTextEditorState.isReadyToProcessActions = true
|
||||||
|
|
@ -182,18 +182,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
|
|
||||||
val sendTypingNotifications by sessionPreferencesStore.isSendTypingNotificationsEnabled().collectAsState(initial = true)
|
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) {
|
LaunchedEffect(attachmentsState.value) {
|
||||||
when (val attachmentStateValue = attachmentsState.value) {
|
when (val attachmentStateValue = attachmentsState.value) {
|
||||||
is AttachmentsState.Sending.Processing -> {
|
is AttachmentsState.Sending.Processing -> {
|
||||||
|
|
@ -267,25 +255,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
loadDraft(textEditorState)
|
loadDraft(markdownTextEditorState, richTextEditorState)
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val mentionSpanProvider = if (isTesting) {
|
val mentionSpanProvider = if (isTesting) {
|
||||||
|
|
@ -338,19 +308,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
attachmentState = attachmentsState,
|
attachmentState = attachmentsState,
|
||||||
)
|
)
|
||||||
is MessageComposerEvents.SetMode -> {
|
is MessageComposerEvents.SetMode -> {
|
||||||
messageComposerContext.composerMode = event.composerMode
|
localCoroutineScope.setMode(event.composerMode, markdownTextEditorState, richTextEditorState)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
|
MessageComposerEvents.AddAttachment -> localCoroutineScope.launch {
|
||||||
showAttachmentSourcePicker = true
|
showAttachmentSourcePicker = true
|
||||||
|
|
@ -398,10 +356,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
is MessageComposerEvents.ToggleTextFormatting -> {
|
is MessageComposerEvents.ToggleTextFormatting -> {
|
||||||
showAttachmentSourcePicker = false
|
showAttachmentSourcePicker = false
|
||||||
showTextFormatting = event.enabled
|
localCoroutineScope.toggleTextFormatting(event.enabled, markdownTextEditorState, richTextEditorState)
|
||||||
if (showTextFormatting) {
|
|
||||||
analyticsService.captureInteraction(Interaction.Name.MobileRoomComposerFormattingEnabled)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
is MessageComposerEvents.Error -> {
|
is MessageComposerEvents.Error -> {
|
||||||
analyticsService.trackError(event.error)
|
analyticsService.trackError(event.error)
|
||||||
|
|
@ -497,7 +452,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is MessageComposerMode.Quote -> TODO()
|
|
||||||
is MessageComposerMode.Reply -> {
|
is MessageComposerMode.Reply -> {
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
|
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
|
||||||
|
|
@ -596,25 +550,33 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.loadDraft(
|
private fun CoroutineScope.loadDraft(
|
||||||
textEditorState: TextEditorState,
|
markdownTextEditorState: MarkdownTextEditorState,
|
||||||
|
richTextEditorState: RichTextEditorState,
|
||||||
) = launch {
|
) = launch {
|
||||||
val draft = draftService.loadDraft(room.roomId) ?: return@launch
|
val draft = draftService.loadDraft(room.roomId) ?: return@launch
|
||||||
val htmlText = draft.htmlText
|
val htmlText = draft.htmlText
|
||||||
val markdownText = draft.plainText
|
val markdownText = draft.plainText
|
||||||
textEditorState.setMarkdown(markdownText)
|
|
||||||
if (htmlText != null) {
|
if (htmlText != null) {
|
||||||
textEditorState.setHtml(htmlText)
|
|
||||||
showTextFormatting = true
|
showTextFormatting = true
|
||||||
|
richTextEditorState.setHtml(htmlText)
|
||||||
|
richTextEditorState.requestFocus()
|
||||||
|
} else {
|
||||||
|
showTextFormatting = false
|
||||||
|
markdownTextEditorState.text.update(markdownText, true)
|
||||||
|
markdownTextEditorState.requestFocusAction()
|
||||||
}
|
}
|
||||||
when (val draftType = draft.draftType) {
|
when (val draftType = draft.draftType) {
|
||||||
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
|
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 -> {
|
is ComposerDraftType.Reply -> {
|
||||||
messageComposerContext.composerMode = MessageComposerMode.Reply(InReplyToDetails.Loading(draftType.eventId))
|
messageComposerContext.composerMode = MessageComposerMode.Reply(InReplyToDetails.Loading(draftType.eventId))
|
||||||
timelineController.invokeOnCurrentTimeline {
|
timelineController.invokeOnCurrentTimeline {
|
||||||
val replyToDetails = loadReplyDetails(draftType.eventId).map(permalinkParser)
|
val replyToDetails = loadReplyDetails(draftType.eventId).map(permalinkParser)
|
||||||
messageComposerContext.composerMode = MessageComposerMode.Reply(replyToDetails)
|
run { messageComposerContext.composerMode = MessageComposerMode.Reply(replyToDetails) }
|
||||||
Unit
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -631,7 +593,6 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mode.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
mode.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
|
||||||
}
|
}
|
||||||
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
|
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
|
||||||
is MessageComposerMode.Quote -> null
|
|
||||||
}
|
}
|
||||||
if (draftType == null || markdown.isBlank()) {
|
if (draftType == null || markdown.isBlank()) {
|
||||||
return@launch
|
return@launch
|
||||||
|
|
@ -644,4 +605,58 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
draftService.saveDraft(room.roomId, composerDraft)
|
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_2
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
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_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.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser
|
||||||
|
|
@ -1083,7 +1082,7 @@ fun anEditMode(
|
||||||
eventId: EventId? = AN_EVENT_ID,
|
eventId: EventId? = AN_EVENT_ID,
|
||||||
message: String = A_MESSAGE,
|
message: String = A_MESSAGE,
|
||||||
transactionId: TransactionId? = null,
|
transactionId: TransactionId? = null,
|
||||||
) = MessageComposerMode.Edit(eventId, message, transactionId)
|
) = MessageComposerMode.Edit(eventId, transactionId, message)
|
||||||
|
|
||||||
fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID))
|
fun aReplyMode() = MessageComposerMode.Reply(replyToDetails = InReplyToDetails.Loading(AN_EVENT_ID))
|
||||||
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
fun aQuoteMode() = MessageComposerMode.Quote(AN_EVENT_ID, A_MESSAGE)
|
||||||
|
|
|
||||||
|
|
@ -601,7 +601,7 @@ internal fun TextComposerEditPreview() = ElementPreview {
|
||||||
ATextComposer(
|
ATextComposer(
|
||||||
TextEditorState.Rich(aRichTextEditorState(initialText = "A message", initialFocus = true)),
|
TextEditorState.Rich(aRichTextEditorState(initialText = "A message", initialFocus = true)),
|
||||||
voiceMessageState = VoiceMessageState.Idle,
|
voiceMessageState = VoiceMessageState.Idle,
|
||||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
|
composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"),
|
||||||
enableVoiceMessages = true,
|
enableVoiceMessages = true,
|
||||||
currentUserId = UserId("@alice:localhost")
|
currentUserId = UserId("@alice:localhost")
|
||||||
)
|
)
|
||||||
|
|
@ -615,7 +615,7 @@ internal fun MarkdownTextComposerEditPreview() = ElementPreview {
|
||||||
ATextComposer(
|
ATextComposer(
|
||||||
TextEditorState.Markdown(aMarkdownTextEditorState(initialText = "A message", initialFocus = true)),
|
TextEditorState.Markdown(aMarkdownTextEditorState(initialText = "A message", initialFocus = true)),
|
||||||
voiceMessageState = VoiceMessageState.Idle,
|
voiceMessageState = VoiceMessageState.Idle,
|
||||||
composerMode = MessageComposerMode.Edit(EventId("$1234"), "Some text", TransactionId("1234")),
|
composerMode = MessageComposerMode.Edit(EventId("$1234"), TransactionId("1234"), "Some text"),
|
||||||
enableVoiceMessages = true,
|
enableVoiceMessages = true,
|
||||||
currentUserId = UserId("@alice:localhost")
|
currentUserId = UserId("@alice:localhost")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ internal fun SendButton(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SendButtonPreview() = ElementPreview {
|
internal fun SendButtonPreview() = ElementPreview {
|
||||||
val normalMode = MessageComposerMode.Normal
|
val normalMode = MessageComposerMode.Normal
|
||||||
val editMode = MessageComposerMode.Edit(null, "", null)
|
val editMode = MessageComposerMode.Edit(null, null, "")
|
||||||
Row {
|
Row {
|
||||||
SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode)
|
SendButton(canSendMessage = true, onClick = {}, composerMode = normalMode)
|
||||||
SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode)
|
SendButton(canSendMessage = false, onClick = {}, composerMode = normalMode)
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,11 @@ sealed interface MessageComposerMode {
|
||||||
|
|
||||||
sealed interface Special : MessageComposerMode
|
sealed interface Special : MessageComposerMode
|
||||||
|
|
||||||
data class Edit(val eventId: EventId?, val content: String, val transactionId: TransactionId?) : Special
|
data class Edit(
|
||||||
|
val eventId: EventId?,
|
||||||
class Quote(val eventId: EventId, val content: String) : Special
|
val transactionId: TransactionId?,
|
||||||
|
val content: String
|
||||||
|
) : Special
|
||||||
|
|
||||||
class Reply(
|
class Reply(
|
||||||
val replyToDetails: InReplyToDetails
|
val replyToDetails: InReplyToDetails
|
||||||
|
|
@ -43,7 +45,6 @@ sealed interface MessageComposerMode {
|
||||||
get() = when (this) {
|
get() = when (this) {
|
||||||
is Normal -> null
|
is Normal -> null
|
||||||
is Edit -> eventId
|
is Edit -> eventId
|
||||||
is Quote -> eventId
|
|
||||||
is Reply -> eventId
|
is Reply -> eventId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue