Skip to content

Commit 82838d6

Browse files
committed
Draft : use the volatile draft store when moving to edit mode
1 parent afd13ab commit 82838d6

File tree

9 files changed

+305
-125
lines changed

9 files changed

+305
-125
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ import io.element.android.libraries.matrix.api.core.RoomId
2020
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
2121

2222
interface ComposerDraftService {
23-
suspend fun loadDraft(roomId: RoomId): ComposerDraft?
24-
suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?)
23+
suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft?
24+
suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean)
2525
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,28 @@ package io.element.android.features.messages.impl.draft
1818

1919
import com.squareup.anvil.annotations.ContributesBinding
2020
import io.element.android.libraries.di.RoomScope
21-
import io.element.android.libraries.matrix.api.MatrixClient
2221
import io.element.android.libraries.matrix.api.core.RoomId
2322
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
24-
import timber.log.Timber
2523
import javax.inject.Inject
2624

2725
@ContributesBinding(RoomScope::class)
2826
class DefaultComposerDraftService @Inject constructor(
29-
private val client: MatrixClient,
27+
private val volatileComposerDraftStore: VolatileComposerDraftStore,
28+
private val matrixComposerDraftStore: MatrixComposerDraftStore,
3029
) : ComposerDraftService {
31-
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {
32-
return client.getRoom(roomId)?.use { room ->
33-
room.loadComposerDraft()
34-
.onFailure {
35-
Timber.e(it, "Failed to load composer draft for room $roomId")
36-
}
37-
.onSuccess { draft ->
38-
room.clearComposerDraft()
39-
Timber.d("Loaded composer draft for room $roomId : $draft")
40-
}
41-
.getOrNull()
30+
override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? {
31+
return if (isVolatile) {
32+
volatileComposerDraftStore.loadDraft(roomId)
33+
} else {
34+
matrixComposerDraftStore.loadDraft(roomId)
4235
}
4336
}
4437

45-
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) {
46-
client.getRoom(roomId)?.use { room ->
47-
val updateDraftResult = if (draft == null) {
48-
room.clearComposerDraft()
49-
} else {
50-
room.saveComposerDraft(draft)
51-
}
52-
updateDraftResult
53-
.onFailure {
54-
Timber.e(it, "Failed to update composer draft for room $roomId")
55-
}
56-
.onSuccess {
57-
Timber.d("Updated composer draft for room $roomId")
58-
}
38+
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) {
39+
if (isVolatile) {
40+
volatileComposerDraftStore.updateDraft(roomId, draft)
41+
} else {
42+
matrixComposerDraftStore.updateDraft(roomId, draft)
5943
}
6044
}
6145
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import javax.inject.Inject
2525
class MatrixComposerDraftStore @Inject constructor(
2626
private val client: MatrixClient,
2727
) : ComposerDraftStore {
28-
2928
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {
3029
return client.getRoom(roomId)?.use { room ->
3130
room.loadComposerDraft()

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
2121
import javax.inject.Inject
2222

2323
class VolatileComposerDraftStore @Inject constructor() : ComposerDraftStore {
24-
2524
private val drafts: MutableMap<RoomId, ComposerDraft> = mutableMapOf()
2625

2726
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

Lines changed: 122 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,10 @@ class MessageComposerPresenter @Inject constructor(
253253
)
254254

255255
LaunchedEffect(Unit) {
256-
loadDraft(markdownTextEditorState, richTextEditorState)
256+
val draft = draftService.loadDraft(room.roomId, isVolatile = false)
257+
if (draft != null) {
258+
applyDraft(draft, markdownTextEditorState, richTextEditorState)
259+
}
257260
}
258261

259262
val mentionSpanProvider = LocalMentionSpanProvider.current
@@ -264,26 +267,16 @@ class MessageComposerPresenter @Inject constructor(
264267
MessageComposerEvents.CloseSpecialMode -> {
265268
if (messageComposerContext.composerMode is MessageComposerMode.Edit) {
266269
localCoroutineScope.launch {
267-
textEditorState.reset()
270+
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = true)
268271
}
272+
} else {
273+
messageComposerContext.composerMode = MessageComposerMode.Normal
269274
}
270-
messageComposerContext.composerMode = MessageComposerMode.Normal
271275
}
272276
is MessageComposerEvents.SendMessage -> {
273-
val html = if (showTextFormatting) {
274-
richTextEditorState.messageHtml
275-
} else {
276-
null
277-
}
278-
val markdown = if (showTextFormatting) {
279-
richTextEditorState.messageMarkdown
280-
} else {
281-
markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
282-
}
283277
appCoroutineScope.sendMessage(
284-
message = Message(html = html, markdown = markdown),
285-
updateComposerMode = { messageComposerContext.composerMode = it },
286-
textEditorState = textEditorState,
278+
markdownTextEditorState = markdownTextEditorState,
279+
richTextEditorState = richTextEditorState,
287280
)
288281
}
289282
is MessageComposerEvents.SendUri -> appCoroutineScope.sendAttachment(
@@ -386,7 +379,8 @@ class MessageComposerPresenter @Inject constructor(
386379
}
387380
}
388381
MessageComposerEvents.SaveDraft -> {
389-
appCoroutineScope.saveDraft(textEditorState)
382+
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
383+
appCoroutineScope.updateDraft(draft, isVolatile = false)
390384
}
391385
}
392386
}
@@ -407,42 +401,26 @@ class MessageComposerPresenter @Inject constructor(
407401
}
408402

409403
private fun CoroutineScope.sendMessage(
410-
message: Message,
411-
updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit,
412-
textEditorState: TextEditorState,
404+
markdownTextEditorState: MarkdownTextEditorState,
405+
richTextEditorState: RichTextEditorState,
413406
) = launch {
407+
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = true)
414408
val capturedMode = messageComposerContext.composerMode
415-
val mentions = when (textEditorState) {
416-
is TextEditorState.Rich -> {
417-
textEditorState.richTextEditorState.mentionsState?.let { state ->
418-
buildList {
419-
if (state.hasAtRoomMention) {
420-
add(Mention.AtRoom)
421-
}
422-
for (userId in state.userIds) {
423-
add(Mention.User(UserId(userId)))
424-
}
425-
}
426-
}.orEmpty()
427-
}
428-
is TextEditorState.Markdown -> textEditorState.state.getMentions()
429-
}
430409
// Reset composer right away
431-
textEditorState.reset()
432-
updateComposerMode(MessageComposerMode.Normal)
410+
resetComposer(markdownTextEditorState, richTextEditorState, fromEdit = capturedMode is MessageComposerMode.Edit)
433411
when (capturedMode) {
434-
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = mentions)
412+
is MessageComposerMode.Normal -> room.sendMessage(body = message.markdown, htmlBody = message.html, mentions = message.mentions)
435413
is MessageComposerMode.Edit -> {
436414
val eventId = capturedMode.eventId
437415
val transactionId = capturedMode.transactionId
438416
timelineController.invokeOnCurrentTimeline {
439-
editMessage(eventId, transactionId, message.markdown, message.html, mentions)
417+
editMessage(eventId, transactionId, message.markdown, message.html, message.mentions)
440418
}
441419
}
442420

443421
is MessageComposerMode.Reply -> {
444422
timelineController.invokeOnCurrentTimeline {
445-
replyMessage(capturedMode.eventId, message.markdown, message.html, mentions)
423+
replyMessage(capturedMode.eventId, message.markdown, message.html, message.mentions)
446424
}
447425
}
448426
}
@@ -537,21 +515,30 @@ class MessageComposerPresenter @Inject constructor(
537515
}
538516
}
539517

540-
private fun CoroutineScope.loadDraft(
518+
private fun CoroutineScope.updateDraft(
519+
draft: ComposerDraft?,
520+
isVolatile: Boolean,
521+
) = launch {
522+
draftService.updateDraft(
523+
roomId = room.roomId,
524+
draft = draft,
525+
isVolatile = isVolatile
526+
)
527+
}
528+
529+
private suspend fun applyDraft(
530+
draft: ComposerDraft,
541531
markdownTextEditorState: MarkdownTextEditorState,
542532
richTextEditorState: RichTextEditorState,
543-
) = launch {
544-
val draft = draftService.loadDraft(room.roomId) ?: return@launch
533+
) {
545534
val htmlText = draft.htmlText
546535
val markdownText = draft.plainText
547536
if (htmlText != null) {
548537
showTextFormatting = true
549-
richTextEditorState.setHtml(htmlText)
550-
richTextEditorState.requestFocus()
538+
setText(htmlText, markdownTextEditorState, richTextEditorState, requestFocus = true)
551539
} else {
552540
showTextFormatting = false
553-
markdownTextEditorState.text.update(markdownText, true)
554-
markdownTextEditorState.requestFocusAction()
541+
setText(markdownText, markdownTextEditorState, richTextEditorState, requestFocus = true)
555542
}
556543
when (val draftType = draft.draftType) {
557544
ComposerDraftType.NewMessage -> messageComposerContext.composerMode = MessageComposerMode.Normal
@@ -570,34 +557,66 @@ class MessageComposerPresenter @Inject constructor(
570557
}
571558
}
572559

573-
private fun CoroutineScope.saveDraft(
574-
textEditorState: TextEditorState,
575-
) = launch {
576-
val html = textEditorState.messageHtml()
577-
val markdown = textEditorState.messageMarkdown(permalinkBuilder)
560+
private fun createDraftFromState(
561+
markdownTextEditorState: MarkdownTextEditorState,
562+
richTextEditorState: RichTextEditorState,
563+
): ComposerDraft? {
564+
val message = currentComposerMessage(markdownTextEditorState, richTextEditorState, withMentions = false)
578565
val draftType = when (val mode = messageComposerContext.composerMode) {
579566
is MessageComposerMode.Normal -> ComposerDraftType.NewMessage
580567
is MessageComposerMode.Edit -> {
581568
mode.eventId?.let { eventId -> ComposerDraftType.Edit(eventId) }
582569
}
583570
is MessageComposerMode.Reply -> ComposerDraftType.Reply(mode.eventId)
584571
}
585-
val composerDraft = if (draftType == null || markdown.isBlank()) {
572+
return if (draftType == null || message.markdown.isBlank()) {
586573
null
587574
} else {
588575
ComposerDraft(
589576
draftType = draftType,
590-
htmlText = html,
591-
plainText = markdown,
577+
htmlText = message.html,
578+
plainText = message.markdown,
592579
)
593580
}
594-
draftService.updateDraft(room.roomId, composerDraft)
581+
}
582+
583+
private fun currentComposerMessage(
584+
markdownTextEditorState: MarkdownTextEditorState,
585+
richTextEditorState: RichTextEditorState,
586+
withMentions: Boolean,
587+
): Message {
588+
return if (showTextFormatting) {
589+
val html = richTextEditorState.messageHtml
590+
val markdown = richTextEditorState.messageMarkdown
591+
val mentions = richTextEditorState.mentionsState
592+
.takeIf { withMentions }
593+
?.let { state ->
594+
buildList {
595+
if (state.hasAtRoomMention) {
596+
add(Mention.AtRoom)
597+
}
598+
for (userId in state.userIds) {
599+
add(Mention.User(UserId(userId)))
600+
}
601+
}
602+
}
603+
.orEmpty()
604+
Message(html = html, markdown = markdown, mentions = mentions)
605+
} else {
606+
val markdown = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
607+
val mentions = if (withMentions) {
608+
markdownTextEditorState.getMentions()
609+
} else {
610+
emptyList()
611+
}
612+
Message(html = null, markdown = markdown, mentions = mentions)
613+
}
595614
}
596615

597616
private fun CoroutineScope.toggleTextFormatting(
598617
enabled: Boolean,
599618
markdownTextEditorState: MarkdownTextEditorState,
600-
richTextEditorState: RichTextEditorState,
619+
richTextEditorState: RichTextEditorState
601620
) = launch {
602621
showTextFormatting = enabled
603622
if (showTextFormatting) {
@@ -615,24 +634,63 @@ class MessageComposerPresenter @Inject constructor(
615634
}
616635

617636
private fun CoroutineScope.setMode(
618-
composerMode: MessageComposerMode,
637+
newComposerMode: MessageComposerMode,
619638
markdownTextEditorState: MarkdownTextEditorState,
620-
richTextEditorState: RichTextEditorState
639+
richTextEditorState: RichTextEditorState,
621640
) = launch {
622-
messageComposerContext.composerMode = composerMode
623-
when (composerMode) {
641+
val currentComposerMode = messageComposerContext.composerMode
642+
when (newComposerMode) {
624643
is MessageComposerMode.Edit -> {
625-
setText(composerMode.content, markdownTextEditorState, richTextEditorState)
644+
if (currentComposerMode !is MessageComposerMode.Edit) {
645+
val draft = createDraftFromState(markdownTextEditorState, richTextEditorState)
646+
updateDraft(draft, isVolatile = true).join()
647+
}
648+
setText(newComposerMode.content, markdownTextEditorState, richTextEditorState)
649+
}
650+
else -> {
651+
// When coming from edit, just clear the composer as it'd be weird to reset a volatile draft in this scenario.
652+
if (currentComposerMode is MessageComposerMode.Edit) {
653+
setText("", markdownTextEditorState, richTextEditorState)
654+
}
626655
}
627-
else -> Unit
628656
}
657+
messageComposerContext.composerMode = newComposerMode
629658
}
630659

631-
private suspend fun setText(content: String, markdownTextEditorState: MarkdownTextEditorState, richTextEditorState: RichTextEditorState) {
660+
private suspend fun resetComposer(
661+
markdownTextEditorState: MarkdownTextEditorState,
662+
richTextEditorState: RichTextEditorState,
663+
fromEdit: Boolean,
664+
) {
665+
// Use the volatile draft only when coming from edit mode otherwise.
666+
val draft = draftService.loadDraft(room.roomId, isVolatile = true).takeIf { fromEdit }
667+
if (draft != null) {
668+
applyDraft(draft, markdownTextEditorState, richTextEditorState)
669+
} else {
670+
setText("", markdownTextEditorState, richTextEditorState)
671+
messageComposerContext.composerMode = MessageComposerMode.Normal
672+
}
673+
}
674+
675+
private suspend fun setText(
676+
content: String,
677+
markdownTextEditorState: MarkdownTextEditorState,
678+
richTextEditorState: RichTextEditorState,
679+
requestFocus: Boolean = false,
680+
) {
632681
if (showTextFormatting) {
633682
richTextEditorState.setHtml(content)
683+
if (requestFocus) {
684+
richTextEditorState.requestFocus()
685+
}
634686
} else {
687+
if (content.isEmpty()) {
688+
markdownTextEditorState.selection = IntRange.EMPTY
689+
}
635690
markdownTextEditorState.text.update(content, true)
691+
if (requestFocus) {
692+
markdownTextEditorState.requestFocusAction()
693+
}
636694
}
637695
}
638696
}

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import io.element.android.libraries.matrix.api.core.RoomId
2020
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
2121

2222
class FakeComposerDraftService : ComposerDraftService {
23-
var loadDraftLambda: (RoomId) -> ComposerDraft? = { null }
24-
override suspend fun loadDraft(roomId: RoomId) = loadDraftLambda(roomId)
23+
var loadDraftLambda: (RoomId, Boolean) -> ComposerDraft? = { _, _ -> null }
24+
override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? = loadDraftLambda(roomId, isVolatile)
2525

26-
var saveDraftLambda: (RoomId, ComposerDraft?) -> Unit = { _, _ -> }
27-
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) = saveDraftLambda(roomId, draft)
26+
var saveDraftLambda: (RoomId, ComposerDraft?, Boolean) -> Unit = { _, _, _ -> }
27+
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) = saveDraftLambda(roomId, draft, isVolatile)
2828
}

0 commit comments

Comments
 (0)