Draft : use the volatile draft store when moving to edit mode
This commit is contained in:
parent
afd13ab22f
commit
82838d6ea5
9 changed files with 305 additions and 125 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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? {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue