Browse Source

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

pull/3132/head
ganfra 3 months ago
parent
commit
82838d6ea5
  1. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/ComposerDraftService.kt
  2. 40
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/DefaultComposerDraftService.kt
  3. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/MatrixComposerDraftStore.kt
  4. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStore.kt
  5. 186
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  6. 8
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/FakeComposerDraftService.kt
  7. 56
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/draft/VolatileComposerDraftStoreTest.kt
  8. 131
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt
  9. 3
      libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Message.kt

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

@ -20,6 +20,6 @@ import io.element.android.libraries.matrix.api.core.RoomId @@ -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)
}

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

@ -18,44 +18,28 @@ package io.element.android.features.messages.impl.draft @@ -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)
}
}
}

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

@ -25,7 +25,6 @@ import javax.inject.Inject @@ -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()

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

@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.room.draft.ComposerDraft @@ -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? {

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

@ -253,7 +253,10 @@ class MessageComposerPresenter @Inject constructor( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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( @@ -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 -> {
// 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)
}
}
else -> Unit
}
messageComposerContext.composerMode = newComposerMode
}
private suspend fun setText(content: String, markdownTextEditorState: MarkdownTextEditorState, richTextEditorState: RichTextEditorState) {
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,
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()
}
}
}
}

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

@ -20,9 +20,9 @@ import io.element.android.libraries.matrix.api.core.RoomId @@ -20,9 +20,9 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
class FakeComposerDraftService : ComposerDraftService {
var loadDraftLambda: (RoomId) -> ComposerDraft? = { null }
override suspend fun loadDraft(roomId: RoomId) = loadDraftLambda(roomId)
var loadDraftLambda: (RoomId, Boolean) -> ComposerDraft? = { _, _ -> null }
override suspend fun loadDraft(roomId: RoomId, isVolatile: Boolean): ComposerDraft? = loadDraftLambda(roomId, isVolatile)
var saveDraftLambda: (RoomId, ComposerDraft?) -> Unit = { _, _ -> }
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) = saveDraftLambda(roomId, draft)
var saveDraftLambda: (RoomId, ComposerDraft?, Boolean) -> Unit = { _, _, _ -> }
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?, isVolatile: Boolean) = saveDraftLambda(roomId, draft, isVolatile)
}

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

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.draft
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.draft.ComposerDraft
import io.element.android.libraries.matrix.api.room.draft.ComposerDraftType
import io.element.android.libraries.matrix.test.A_ROOM_ID
import kotlinx.coroutines.test.runTest
import org.junit.Test
class VolatileComposerDraftStoreTest {
private val roomId = A_ROOM_ID
private val sut = VolatileComposerDraftStore()
private val draft = ComposerDraft("plainText", "htmlText", ComposerDraftType.NewMessage)
@Test
fun `when storing a non-null draft and then loading it, it's loaded and removed`() = runTest {
val initialDraft = sut.loadDraft(roomId)
assertThat(initialDraft).isNull()
sut.updateDraft(roomId, draft)
val loadedDraft = sut.loadDraft(roomId)
assertThat(loadedDraft).isEqualTo(draft)
val loadedDraftAfter = sut.loadDraft(roomId)
assertThat(loadedDraftAfter).isNull()
}
@Test
fun `when storing a null draft and then loading it, it's removing the previous one`() = runTest {
val initialDraft = sut.loadDraft(roomId)
assertThat(initialDraft).isNull()
sut.updateDraft(roomId, draft)
sut.updateDraft(roomId, null)
val loadedDraft = sut.loadDraft(roomId)
assertThat(loadedDraft).isNull()
}
}

131
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/textcomposer/MessageComposerPresenterTest.kt

@ -172,21 +172,85 @@ class MessageComposerPresenterTest { @@ -172,21 +172,85 @@ class MessageComposerPresenterTest {
@Test
fun `present - change mode to edit`() = runTest {
val presenter = createPresenter(this)
val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean ->
ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage)
}
val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> }
val draftService = FakeComposerDraftService().apply {
this.loadDraftLambda = loadDraftLambda
this.saveDraftLambda = updateDraftLambda
}
val presenter = createPresenter(
coroutineScope = this,
draftService = draftService,
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
remember(state, state.textEditorState.messageHtml()) { state }
}.test {
var state = awaitFirstItem()
val mode = anEditMode()
val mode = anEditMode(message = ANOTHER_MESSAGE)
state.eventSink.invoke(MessageComposerEvents.SetMode(mode))
state = awaitItem()
assertThat(state.mode).isEqualTo(mode)
assertThat(state.textEditorState.messageHtml()).isEqualTo(ANOTHER_MESSAGE)
state = backToNormalMode(state)
// The message that was being edited is cleared and volatile draft is loaded
assertThat(state.textEditorState.messageHtml()).isEqualTo(A_MESSAGE)
state = backToNormalMode(state, skipCount = 1)
// The message that was being edited is cleared
assertThat(state.textEditorState.messageHtml()).isEqualTo("")
assert(loadDraftLambda)
.isCalledExactly(2)
.withSequence(
// Automatic load of draft
listOf(value(A_ROOM_ID), value(false)),
// Load of volatile draft when closing edit mode
listOf(value(A_ROOM_ID), value(true))
)
assert(updateDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID), any(), value(true))
}
}
@Test
fun `present - change mode to reply after edit`() = runTest {
val loadDraftLambda = lambdaRecorder { _: RoomId, _: Boolean ->
ComposerDraft(A_MESSAGE, A_MESSAGE, ComposerDraftType.NewMessage)
}
val updateDraftLambda = lambdaRecorder { _: RoomId, _: ComposerDraft?, _: Boolean -> }
val draftService = FakeComposerDraftService().apply {
this.loadDraftLambda = loadDraftLambda
this.saveDraftLambda = updateDraftLambda
}
val presenter = createPresenter(
coroutineScope = this,
draftService = draftService,
)
moleculeFlow(RecompositionMode.Immediate) {
val state = presenter.present()
remember(state, state.textEditorState.messageHtml()) { state }
}.test {
var state = awaitFirstItem()
val editMode = anEditMode(message = ANOTHER_MESSAGE)
state.eventSink.invoke(MessageComposerEvents.SetMode(editMode))
state = awaitItem()
assertThat(state.mode).isEqualTo(editMode)
assertThat(state.textEditorState.messageHtml()).isEqualTo(ANOTHER_MESSAGE)
val replyMode = aReplyMode()
state.eventSink.invoke(MessageComposerEvents.SetMode(replyMode))
state = awaitItem()
assertThat(state.mode).isEqualTo(replyMode)
assertThat(state.textEditorState.messageHtml()).isEmpty()
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID), value(false))
assert(updateDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID), any(), value(true))
}
}
@ -316,7 +380,7 @@ class MessageComposerPresenterTest { @@ -316,7 +380,7 @@ class MessageComposerPresenterTest {
assert(editMessageLambda)
.isCalledOnce()
.with(any(), any(), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any())
.with(value(AN_EVENT_ID), value(null), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any())
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
@ -365,7 +429,7 @@ class MessageComposerPresenterTest { @@ -365,7 +429,7 @@ class MessageComposerPresenterTest {
assert(editMessageLambda)
.isCalledOnce()
.with(any(), any(), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any())
.with(value(null), value(A_TRANSACTION_ID), value(ANOTHER_MESSAGE), value(ANOTHER_MESSAGE), any())
assertThat(analyticsService.capturedEvents).containsExactly(
Composer(
@ -1013,7 +1077,7 @@ class MessageComposerPresenterTest { @@ -1013,7 +1077,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - when there is no draft, nothing is restored`() = runTest {
val loadDraftLambda = lambdaRecorder<RoomId, ComposerDraft?> { _ -> null }
val loadDraftLambda = lambdaRecorder<RoomId, Boolean, ComposerDraft?> { _, _ -> null }
val composerDraftService = FakeComposerDraftService().apply {
this.loadDraftLambda = loadDraftLambda
}
@ -1024,7 +1088,7 @@ class MessageComposerPresenterTest { @@ -1024,7 +1088,7 @@ class MessageComposerPresenterTest {
awaitFirstItem()
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID))
.with(value(A_ROOM_ID), value(false))
ensureAllEventsConsumed()
}
@ -1032,7 +1096,7 @@ class MessageComposerPresenterTest { @@ -1032,7 +1096,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - when there is a draft for new message with plain text, it is restored`() = runTest {
val loadDraftLambda = lambdaRecorder<RoomId, ComposerDraft?> { _ ->
val loadDraftLambda = lambdaRecorder<RoomId, Boolean, ComposerDraft?> { _, _ ->
ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage)
}
val composerDraftService = FakeComposerDraftService().apply {
@ -1054,7 +1118,7 @@ class MessageComposerPresenterTest { @@ -1054,7 +1118,7 @@ class MessageComposerPresenterTest {
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID))
.with(value(A_ROOM_ID), value(false))
ensureAllEventsConsumed()
}
@ -1062,7 +1126,7 @@ class MessageComposerPresenterTest { @@ -1062,7 +1126,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - when there is a draft for new message with rich text, it is restored`() = runTest {
val loadDraftLambda = lambdaRecorder<RoomId, ComposerDraft?> { _ ->
val loadDraftLambda = lambdaRecorder<RoomId, Boolean, ComposerDraft?> { _, _ ->
ComposerDraft(
plainText = A_MESSAGE,
htmlText = A_MESSAGE,
@ -1088,14 +1152,14 @@ class MessageComposerPresenterTest { @@ -1088,14 +1152,14 @@ class MessageComposerPresenterTest {
}
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID))
.with(value(A_ROOM_ID), value(false))
ensureAllEventsConsumed()
}
}
@Test
fun `present - when there is a draft for edit, it is restored`() = runTest {
val loadDraftLambda = lambdaRecorder<RoomId, ComposerDraft?> { _ ->
val loadDraftLambda = lambdaRecorder<RoomId, Boolean, ComposerDraft?> { _, _ ->
ComposerDraft(
plainText = A_MESSAGE,
htmlText = null,
@ -1122,7 +1186,7 @@ class MessageComposerPresenterTest { @@ -1122,7 +1186,7 @@ class MessageComposerPresenterTest {
}
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID))
.with(value(A_ROOM_ID), value(false))
ensureAllEventsConsumed()
}
@ -1130,7 +1194,7 @@ class MessageComposerPresenterTest { @@ -1130,7 +1194,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - when there is a draft for reply, it is restored`() = runTest {
val loadDraftLambda = lambdaRecorder<RoomId, ComposerDraft?> { _ ->
val loadDraftLambda = lambdaRecorder<RoomId, Boolean, ComposerDraft?> { _, _ ->
ComposerDraft(
plainText = A_MESSAGE,
htmlText = null,
@ -1165,7 +1229,7 @@ class MessageComposerPresenterTest { @@ -1165,7 +1229,7 @@ class MessageComposerPresenterTest {
}
assert(loadDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID))
.with(value(A_ROOM_ID), value(false))
assert(loadReplyDetailsLambda)
.isCalledOnce()
@ -1177,7 +1241,7 @@ class MessageComposerPresenterTest { @@ -1177,7 +1241,7 @@ class MessageComposerPresenterTest {
@Test
fun `present - when save draft event is invoked and composer is empty then service is called with null draft`() = runTest {
val saveDraftLambda = lambdaRecorder<RoomId, ComposerDraft?, Unit> { _, _ -> }
val saveDraftLambda = lambdaRecorder<RoomId, ComposerDraft?, Boolean, Unit> { _, _, _ -> }
val composerDraftService = FakeComposerDraftService().apply {
this.saveDraftLambda = saveDraftLambda
}
@ -1190,13 +1254,13 @@ class MessageComposerPresenterTest { @@ -1190,13 +1254,13 @@ class MessageComposerPresenterTest {
advanceUntilIdle()
assert(saveDraftLambda)
.isCalledOnce()
.with(value(A_ROOM_ID), value(null))
.with(value(A_ROOM_ID), value(null), value(false))
}
}
@Test
fun `present - when save draft event is invoked and composer is not empty then service is called`() = runTest {
val saveDraftLambda = lambdaRecorder<RoomId, ComposerDraft?, Unit> { _, _ -> }
val saveDraftLambda = lambdaRecorder<RoomId, ComposerDraft?, Boolean, Unit> { _, _, _ -> }
val composerDraftService = FakeComposerDraftService().apply {
this.saveDraftLambda = saveDraftLambda
}
@ -1240,17 +1304,34 @@ class MessageComposerPresenterTest { @@ -1240,17 +1304,34 @@ class MessageComposerPresenterTest {
advanceUntilIdle()
assert(saveDraftLambda)
.isCalledExactly(4)
.isCalledExactly(5)
.withSequence(
listOf(value(A_ROOM_ID), value(ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage))),
listOf(value(A_ROOM_ID), value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage))),
listOf(
value(A_ROOM_ID),
value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.Edit(AN_EVENT_ID)))
value(ComposerDraft(plainText = A_MESSAGE, htmlText = null, draftType = ComposerDraftType.NewMessage)),
value(false)
),
listOf(
value(A_ROOM_ID),
value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage)),
value(false)
),
listOf(
value(A_ROOM_ID),
value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.NewMessage)),
// The volatile draft created when switching to edit mode.
value(true)
),
listOf(
value(A_ROOM_ID),
value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.Edit(AN_EVENT_ID))),
value(false)
),
listOf(
value(A_ROOM_ID),
value(ComposerDraft(plainText = A_MESSAGE, htmlText = A_MESSAGE, draftType = ComposerDraftType.Reply(AN_EVENT_ID)))
// When moving from edit mode, text composer is cleared, so the draft is null
value(null),
value(false)
)
)
}

3
libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/model/Message.kt

@ -16,7 +16,10 @@ @@ -16,7 +16,10 @@
package io.element.android.libraries.textcomposer.model
import io.element.android.libraries.matrix.api.room.Mention
data class Message(
val html: String?,
val markdown: String,
val mentions: List<Mention>,
)

Loading…
Cancel
Save