Draft : use the volatile draft store when moving to edit mode

This commit is contained in:
ganfra 2024-07-03 11:16:00 +02:00
parent afd13ab22f
commit 82838d6ea5
9 changed files with 305 additions and 125 deletions

View file

@ -20,6 +20,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
interface ComposerDraftService {
suspend fun loadDraft(roomId: RoomId): ComposerDraft?
suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?)
suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft?
suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean)
}

View file

@ -18,44 +18,28 @@ package io.element.android.features.messages.impl.draft
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import timber.log.Timber
import javax.inject.Inject
@ContributesBinding(RoomScope::class)
class DefaultComposerDraftService @Inject constructor(
private val client: MatrixClient,
private val volatileComposerDraftStore: VolatileComposerDraftStore,
private val matrixComposerDraftStore: MatrixComposerDraftStore,
) : ComposerDraftService {
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {
return client.getRoom(roomId)?.use { room ->
room.loadComposerDraft()
.onFailure {
Timber.e(it, "Failed to load composer draft for room $roomId")
}
.onSuccess { draft ->
room.clearComposerDraft()
Timber.d("Loaded composer draft for room $roomId : $draft")
}
.getOrNull()
override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? {
return if (isVolatile) {
volatileComposerDraftStore.loadDraft(roomId)
} else {
matrixComposerDraftStore.loadDraft(roomId)
}
}
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) {
client.getRoom(roomId)?.use { room ->
val updateDraftResult = if (draft == null) {
room.clearComposerDraft()
} else {
room.saveComposerDraft(draft)
}
updateDraftResult
.onFailure {
Timber.e(it, "Failed to update composer draft for room $roomId")
}
.onSuccess {
Timber.d("Updated composer draft for room $roomId")
}
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) {
if (isVolatile) {
volatileComposerDraftStore.updateDraft(roomId, draft)
} else {
matrixComposerDraftStore.updateDraft(roomId, draft)
}
}
}

View file

@ -25,7 +25,6 @@ import javax.inject.Inject
class MatrixComposerDraftStore @Inject constructor(
private val client: MatrixClient,
) : ComposerDraftStore {
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {
return client.getRoom(roomId)?.use { room ->
room.loadComposerDraft()

View file

@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import javax.inject.Inject
class VolatileComposerDraftStore @Inject constructor() : ComposerDraftStore {
private val drafts: MutableMap<RoomId, ComposerDraft> = mutableMapOf()
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {

View file

@ -253,7 +253,10 @@ class MessageComposerPresenter @Inject constructor(
)
LaunchedEffect(Unit) {
loadDraft(markdownTextEditorState, richTextEditorState)
val draft = draftService.loadDraft(room.roomId, isVolatile = false)
if (draft != null) {
applyDraft(draft, markdownTextEditorState, richTextEditorState)
}
}
val mentionSpanProvider = LocalMentionSpanProvider.current
@ -264,26 +267,16 @@ class MessageComposerPresenter @Inject constructor(
MessageComposerEvents.CloseSpecialMode -> {
if (messageComposerContext.composerMode is MessageComposerMode.Edit) {
localCoroutineScope.launch {
textEditorState.reset()
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = true)
}
} else {
messageComposerContext.composerMode = MessageComposerMode.Normal
}
messageComposerContext.composerMode = MessageComposerMode.Normal
}
is MessageComposerEvents.SendMessage -> {
val html = if (showTextFormatting) {
richTextEditorState.messageHtml
} else {
null
}
val markdown = if (showTextFormatting) {
richTextEditorState.messageMarkdown
} else {
markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
}
appCoroutineScope.sendMessage(
message = Message(html = html, markdown = markdown),
updateComposerMode = { messageComposerContext.composerMode = it },
textEditorState = textEditorState,
markdownTextEditorState = markdownTextEditorState,
richTextEditorState = richTextEditorState,
)
}
is MessageComposerEvents.SendUri -> appCoroutineScope.sendAttachment(
@ -386,7 +379,8 @@ class MessageComposerPresenter @Inject constructor(
}
}
MessageComposerEvents.SaveDraft -> {
appCoroutineScope.saveDraft(textEditorState)
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
appCoroutineScope.updateDraft(draft, isVolatile = false)
}
}
}
@ -407,42 +401,26 @@ class MessageComposerPresenter @Inject constructor(
}
private fun CoroutineScope.sendMessage(
message: Message,
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
textEditorState: TextEditorState,
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
) = launch {
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = true)
val capturedMode = messageComposerContext.composerMode
val mentions = when (textEditorState) {
is TextEditorState.Rich -> {
textEditorState.richTextEditorState.mentionsState?.let { state ->
buildList {
if (state.hasAtRoomMention) {
add(Mention.AtRoom)
}
for (userId in state.userIds) {
add(Mention.User(UserId(userId)))
}
}
}.orEmpty()
}
is TextEditorState.Markdown -> textEditorState.state.getMentions()
}
// Reset composer right away
textEditorState.reset()
updateComposerMode(MessageComposerMode.Normal)
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
when (capturedMode) {
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = mentions)
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = message.mentions)
is MessageComposerMode.Edit -> {
val eventId = capturedMode.eventId
val transactionId = capturedMode.transactionId
timelineController.invokeOnCurrentTimeline {
editMessage(eventId, transactionId, message.markdown, message.html, mentions)
editMessage(eventId, transactionId, message.markdown, message.html, message.mentions)
}
}
is MessageComposerMode.Reply -> {
timelineController.invokeOnCurrentTimeline {
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
replyMessage(capturedMode.eventId, message.markdown, message.html, message.mentions)
}
}
}
@ -537,21 +515,30 @@ class MessageComposerPresenter @Inject constructor(
}
}
private fun CoroutineScope.loadDraft(
private fun CoroutineScope.updateDraft(
draft: ComposerDraft?,
isVolatile: Boolean,
) = launch {
draftService.updateDraft(
roomId = room.roomId,
draft = draft,
isVolatile = isVolatile
)
}
private suspend fun applyDraft(
draft: ComposerDraft,
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
) = launch {
val draft = draftService.loadDraft(room.roomId) ?: return@launch
) {
val htmlText = draft.htmlText
val markdownText = draft.plainText
if (htmlText != null) {
showTextFormatting = true
richTextEditorState.setHtml(htmlText)
richTextEditorState.requestFocus()
setText(htmlText, markdownTextEditorState, richTextEditorState, requestFocus = true)
} else {
showTextFormatting = false
markdownTextEditorState.text.update(markdownText, true)
markdownTextEditorState.requestFocusAction()
setText(markdownText, markdownTextEditorState, richTextEditorState, requestFocus = true)
}
when (val draftType = draft.draftType) {
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
@ -570,11 +557,11 @@ class MessageComposerPresenter @Inject constructor(
}
}
private fun CoroutineScope.saveDraft(
textEditorState: TextEditorState,
) = launch {
val html = textEditorState.messageHtml()
val markdown = textEditorState.messageMarkdown(permalinkBuilder)
private fun createDraftFromState(
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
): ComposerDraft? {
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
val draftType = when (val mode = messageComposerContext.composerMode) {
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
is MessageComposerMode.Edit -> {
@ -582,22 +569,54 @@ class MessageComposerPresenter @Inject constructor(
}
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
}
val composerDraft = if (draftType == null || markdown.isBlank()) {
return if (draftType == null || message.markdown.isBlank()) {
null
} else {
ComposerDraft(
draftType = draftType,
htmlText = html,
plainText = markdown,
htmlText = message.html,
plainText = message.markdown,
)
}
draftService.updateDraft(room.roomId, composerDraft)
}
private fun currentComposerMessage(
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
withMentions: Boolean,
): Message {
return if (showTextFormatting) {
val html = richTextEditorState.messageHtml
val markdown = richTextEditorState.messageMarkdown
val mentions = richTextEditorState.mentionsState
.takeIf { withMentions }
?.let { state ->
buildList {
if (state.hasAtRoomMention) {
add(Mention.AtRoom)
}
for (userId in state.userIds) {
add(Mention.User(UserId(userId)))
}
}
}
.orEmpty()
Message(html = html, markdown = markdown, mentions = mentions)
} else {
val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
val mentions = if (withMentions) {
markdownTextEditorState.getMentions()
} else {
emptyList()
}
Message(html = null, markdown = markdown, mentions = mentions)
}
}
private fun CoroutineScope.toggleTextFormatting(
enabled: Boolean,
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
richTextEditorState: RichTextEditorState
) = launch {
showTextFormatting = enabled
if (showTextFormatting) {
@ -615,24 +634,63 @@ class MessageComposerPresenter @Inject constructor(
}
private fun CoroutineScope.setMode(
composerMode: MessageComposerMode,
newComposerMode: MessageComposerMode,
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState
richTextEditorState: RichTextEditorState,
) = launch {
messageComposerContext.composerMode = composerMode
when (composerMode) {
val currentComposerMode = messageComposerContext.composerMode
when (newComposerMode) {
is MessageComposerMode.Edit -> {
setText(composerMode.content, markdownTextEditorState, richTextEditorState)
if (currentComposerMode !is MessageComposerMode.Edit) {
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
updateDraft(draft, isVolatile = true).join()
}
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState)
}
else -> Unit
else -> {
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
if (currentComposerMode is MessageComposerMode.Edit) {
setText("", markdownTextEditorState, richTextEditorState)
}
}
}
messageComposerContext.composerMode = newComposerMode
}
private suspend fun resetComposer(
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
fromEdit: Boolean,
) {
// Use the volatile draft only when coming from edit mode otherwise.
val draft = draftService.loadDraft(room.roomId, isVolatile = true).takeIf { fromEdit }
if (draft != null) {
applyDraft(draft, markdownTextEditorState, richTextEditorState)
} else {
setText("", markdownTextEditorState, richTextEditorState)
messageComposerContext.composerMode = MessageComposerMode.Normal
}
}
private suspend fun setText(content: String, markdownTextEditorState: MarkdownTextEditorState, richTextEditorState: RichTextEditorState) {
private suspend fun setText(
content: String,
markdownTextEditorState: MarkdownTextEditorState,
richTextEditorState: RichTextEditorState,
requestFocus: Boolean = false,
) {
if (showTextFormatting) {
richTextEditorState.setHtml(content)
if (requestFocus) {
richTextEditorState.requestFocus()
}
} else {
if (content.isEmpty()) {
markdownTextEditorState.selection = IntRange.EMPTY
}
markdownTextEditorState.text.update(content, true)
if (requestFocus) {
markdownTextEditorState.requestFocusAction()
}
}
}
}