Merge pull request #3943 from element-hq/feature/bma/_poc/mediaPreprocessing
Hide media preprocessing
This commit is contained in:
commit
d6b653f127
11 changed files with 325 additions and 38 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -19,10 +20,16 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||||
|
import io.element.android.libraries.androidutils.file.safeDelete
|
||||||
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
|
import io.element.android.libraries.mediaupload.api.allFiles
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
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 kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
|
@ -39,6 +46,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
private val mediaSender: MediaSender,
|
private val mediaSender: MediaSender,
|
||||||
private val permalinkBuilder: PermalinkBuilder,
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
private val temporaryUriDeleter: TemporaryUriDeleter,
|
private val temporaryUriDeleter: TemporaryUriDeleter,
|
||||||
|
private val featureFlagsService: FeatureFlagService,
|
||||||
) : Presenter<AttachmentsPreviewState> {
|
) : Presenter<AttachmentsPreviewState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -63,19 +71,62 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
|
val userSentAttachment = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val mediaUploadInfoState = remember { mutableStateOf<AsyncData<MediaUploadInfo>>(AsyncData.Uninitialized) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
preProcessAttachment(
|
||||||
|
attachment,
|
||||||
|
mediaUploadInfoState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(userSentAttachment.value, mediaUploadInfoState.value) {
|
||||||
|
if (userSentAttachment.value) {
|
||||||
|
// User confirmed sending the attachment
|
||||||
|
when (val mediaUploadInfo = mediaUploadInfoState.value) {
|
||||||
|
is AsyncData.Success -> {
|
||||||
|
// Pre-processing is done, send the attachment
|
||||||
|
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
||||||
|
sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo = mediaUploadInfo.data,
|
||||||
|
caption = caption,
|
||||||
|
sendActionState = sendActionState,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is AsyncData.Failure -> {
|
||||||
|
// Pre-processing has failed, show the error
|
||||||
|
sendActionState.value = SendActionState.Failure(mediaUploadInfo.error)
|
||||||
|
}
|
||||||
|
AsyncData.Uninitialized,
|
||||||
|
is AsyncData.Loading -> {
|
||||||
|
// Pre-processing is still in progress, do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||||
when (attachmentsPreviewEvents) {
|
when (attachmentsPreviewEvents) {
|
||||||
is AttachmentsPreviewEvents.SendAttachment -> {
|
is AttachmentsPreviewEvents.SendAttachment -> coroutineScope.launch {
|
||||||
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||||
.takeIf { it.isNotEmpty() }
|
userSentAttachment.value = true
|
||||||
ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(
|
val instantSending = mediaUploadInfoState.value.isReady() && useSendQueue
|
||||||
attachment = attachment,
|
sendActionState.value = if (instantSending) {
|
||||||
caption = caption,
|
SendActionState.Sending.InstantSending
|
||||||
sendActionState = sendActionState,
|
} else {
|
||||||
)
|
SendActionState.Sending.Processing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.Cancel -> {
|
AttachmentsPreviewEvents.Cancel -> {
|
||||||
coroutineScope.cancel(attachment)
|
coroutineScope.cancel(
|
||||||
|
attachment,
|
||||||
|
mediaUploadInfoState.value,
|
||||||
|
sendActionState,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.ClearSendState -> {
|
AttachmentsPreviewEvents.ClearSendState -> {
|
||||||
ongoingSendAttachmentJob.value?.let {
|
ongoingSendAttachmentJob.value?.let {
|
||||||
|
|
@ -95,56 +146,95 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendAttachment(
|
private fun CoroutineScope.preProcessAttachment(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
caption: String?,
|
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
||||||
sendActionState: MutableState<SendActionState>,
|
|
||||||
) = launch {
|
) = launch {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
sendMedia(
|
preProcessMedia(
|
||||||
mediaAttachment = attachment,
|
mediaAttachment = attachment,
|
||||||
caption = caption,
|
mediaUploadInfoState = mediaUploadInfoState,
|
||||||
sendActionState = sendActionState,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun preProcessMedia(
|
||||||
|
mediaAttachment: Attachment.Media,
|
||||||
|
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
||||||
|
) {
|
||||||
|
mediaUploadInfoState.value = AsyncData.Loading()
|
||||||
|
mediaSender.preProcessMedia(
|
||||||
|
uri = mediaAttachment.localMedia.uri,
|
||||||
|
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||||
|
).fold(
|
||||||
|
onSuccess = { mediaUploadInfo ->
|
||||||
|
mediaUploadInfoState.value = AsyncData.Success(mediaUploadInfo)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
Timber.e(it, "Failed to pre-process media")
|
||||||
|
if (it is CancellationException) {
|
||||||
|
throw it
|
||||||
|
} else {
|
||||||
|
mediaUploadInfoState.value = AsyncData.Failure(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.cancel(
|
private fun CoroutineScope.cancel(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
|
mediaUploadInfo: AsyncData<MediaUploadInfo>,
|
||||||
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) = launch {
|
||||||
// Delete the temporary file
|
// Delete the temporary file
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
||||||
|
mediaUploadInfo.dataOrNull()?.let { data ->
|
||||||
|
cleanUp(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reset the sendActionState to ensure that dialog is closed before the screen
|
||||||
|
sendActionState.value = SendActionState.Done
|
||||||
onDoneListener()
|
onDoneListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private fun cleanUp(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
|
) {
|
||||||
|
mediaUploadInfo.allFiles().forEach { file ->
|
||||||
|
file.safeDelete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
caption: String?,
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = runCatching {
|
) = runCatching {
|
||||||
val context = coroutineContext
|
val context = coroutineContext
|
||||||
val progressCallback = object : ProgressCallback {
|
val progressCallback = object : ProgressCallback {
|
||||||
override fun onProgress(current: Long, total: Long) {
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
// Note will not happen if useSendQueue is true
|
||||||
if (context.isActive) {
|
if (context.isActive) {
|
||||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendActionState.value = SendActionState.Sending.Processing
|
mediaSender.sendPreProcessedMedia(
|
||||||
mediaSender.sendMedia(
|
mediaUploadInfo = mediaUploadInfo,
|
||||||
uri = mediaAttachment.localMedia.uri,
|
|
||||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
|
||||||
caption = caption,
|
caption = caption,
|
||||||
|
formattedCaption = null,
|
||||||
progressCallback = progressCallback
|
progressCallback = progressCallback
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
}.fold(
|
}.fold(
|
||||||
onSuccess = {
|
onSuccess = {
|
||||||
|
cleanUp(mediaUploadInfo)
|
||||||
|
// Reset the sendActionState to ensure that dialog is closed before the screen
|
||||||
|
sendActionState.value = SendActionState.Done
|
||||||
onDoneListener()
|
onDoneListener()
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,11 @@ sealed interface SendActionState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface Sending : SendActionState {
|
sealed interface Sending : SendActionState {
|
||||||
|
data object InstantSending : Sending
|
||||||
data object Processing : Sending
|
data object Processing : Sending
|
||||||
data class Uploading(val progress: Float) : Sending
|
data class Uploading(val progress: Float) : Sending
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Failure(val error: Throwable) : SendActionState
|
data class Failure(val error: Throwable) : SendActionState
|
||||||
|
data object Done : SendActionState
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
|
||||||
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
|
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
|
||||||
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
|
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
|
||||||
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
|
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
|
||||||
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,17 @@ private fun AttachmentSendStateView(
|
||||||
onRetryClick: () -> Unit
|
onRetryClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
when (sendActionState) {
|
when (sendActionState) {
|
||||||
is SendActionState.Sending -> {
|
is SendActionState.Sending.Processing -> {
|
||||||
ProgressDialog(
|
ProgressDialog(
|
||||||
type = when (sendActionState) {
|
type = ProgressDialogType.Indeterminate,
|
||||||
is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress)
|
text = stringResource(id = CommonStrings.common_sending),
|
||||||
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
|
showCancelButton = true,
|
||||||
},
|
onDismissRequest = onDismissClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SendActionState.Sending.Uploading -> {
|
||||||
|
ProgressDialog(
|
||||||
|
type = ProgressDialogType.Determinate(sendActionState.progress),
|
||||||
text = stringResource(id = CommonStrings.common_sending),
|
text = stringResource(id = CommonStrings.common_sending),
|
||||||
showCancelButton = true,
|
showCancelButton = true,
|
||||||
onDismissRequest = onDismissClick,
|
onDismissRequest = onDismissClick,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ import io.element.android.features.messages.impl.attachments.preview.OnDoneListe
|
||||||
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||||
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
||||||
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||||
|
|
@ -40,10 +42,12 @@ import io.element.android.libraries.preferences.test.InMemorySessionPreferencesS
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -84,16 +88,142 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media after pre-processing success scenario`() = runTest {
|
||||||
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendFileResult = sendFileResult,
|
||||||
|
)
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor(
|
||||||
|
processLatch = processLatch,
|
||||||
|
),
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
advanceUntilIdle()
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.InstantSending)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media before pre-processing success scenario`() = runTest {
|
||||||
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
|
Result.success(FakeMediaUploadHandler())
|
||||||
|
}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendFileResult = sendFileResult,
|
||||||
|
)
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor(
|
||||||
|
processLatch = processLatch,
|
||||||
|
),
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media with pre-processing failure after user sends media`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor().apply {
|
||||||
|
givenResult(Result.failure(Exception()))
|
||||||
|
},
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - send media with pre-processing failure before user sends media`() = runTest {
|
||||||
|
val room = FakeMatrixRoom()
|
||||||
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
val processLatch = CompletableDeferred<Unit>()
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(
|
||||||
|
room = room,
|
||||||
|
mediaPreProcessor = FakeMediaPreProcessor().apply {
|
||||||
|
givenResult(Result.failure(Exception()))
|
||||||
|
},
|
||||||
|
onDoneListener = { onDoneListener() },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
// Pre-processing finishes
|
||||||
|
processLatch.complete(Unit)
|
||||||
|
advanceUntilIdle()
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.InstantSending)
|
||||||
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - cancel scenario`() = runTest {
|
fun `present - cancel scenario`() = runTest {
|
||||||
val onDoneListener = lambdaRecorder<Unit> { }
|
val onDoneListener = lambdaRecorder<Unit> { }
|
||||||
|
|
@ -108,6 +238,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.Cancel)
|
initialState.eventSink(AttachmentsPreviewEvents.Cancel)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
deleteCallback.assertions().isCalledOnce()
|
deleteCallback.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +270,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendImageResult.assertions().isCalledOnce().with(
|
sendImageResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -177,8 +311,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendVideoResult.assertions().isCalledOnce().with(
|
sendVideoResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -214,8 +350,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
advanceUntilIdle()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendAudioResult.assertions().isCalledOnce().with(
|
sendAudioResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -243,8 +381,9 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
val loadingState = awaitItem()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
|
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
|
@ -263,6 +402,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
|
@ -277,7 +418,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
|
||||||
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
|
||||||
temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(),
|
temporaryUriDeleter: TemporaryUriDeleter = FakeTemporaryUriDeleter(),
|
||||||
onDoneListener: OnDoneListener = OnDoneListener {},
|
onDoneListener: OnDoneListener = OnDoneListener { lambdaError() },
|
||||||
|
mediaUploadOnSendQueueEnabled: Boolean = true,
|
||||||
): AttachmentsPreviewPresenter {
|
): AttachmentsPreviewPresenter {
|
||||||
return AttachmentsPreviewPresenter(
|
return AttachmentsPreviewPresenter(
|
||||||
attachment = aMediaAttachment(localMedia),
|
attachment = aMediaAttachment(localMedia),
|
||||||
|
|
@ -285,6 +427,9 @@ class AttachmentsPreviewPresenterTest {
|
||||||
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
|
||||||
permalinkBuilder = permalinkBuilder,
|
permalinkBuilder = permalinkBuilder,
|
||||||
temporaryUriDeleter = temporaryUriDeleter,
|
temporaryUriDeleter = temporaryUriDeleter,
|
||||||
|
featureFlagsService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(FeatureFlags.MediaUploadOnSendQueue.key to mediaUploadOnSendQueueEnabled),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,35 @@ class MediaSender @Inject constructor(
|
||||||
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
|
||||||
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
|
||||||
|
|
||||||
|
suspend fun preProcessMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
): Result<MediaUploadInfo> {
|
||||||
|
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
|
||||||
|
return preProcessor
|
||||||
|
.process(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
deleteOriginal = false,
|
||||||
|
compressIfPossible = compressIfPossible,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun sendPreProcessedMedia(
|
||||||
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
|
caption: String?,
|
||||||
|
formattedCaption: String?,
|
||||||
|
progressCallback: ProgressCallback?,
|
||||||
|
): Result<Unit> {
|
||||||
|
return room.sendMedia(
|
||||||
|
uploadInfo = mediaUploadInfo,
|
||||||
|
progressCallback = progressCallback,
|
||||||
|
caption = caption,
|
||||||
|
formattedCaption = formattedCaption
|
||||||
|
)
|
||||||
|
.handleSendResult()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun sendMedia(
|
suspend fun sendMedia(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
|
|
|
||||||
|
|
@ -22,3 +22,11 @@ sealed interface MediaUploadInfo {
|
||||||
data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List<Float>) : MediaUploadInfo
|
data class VoiceMessage(override val file: File, val audioInfo: AudioInfo, val waveform: List<Float>) : MediaUploadInfo
|
||||||
data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo
|
data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun MediaUploadInfo.allFiles(): List<File> {
|
||||||
|
return listOfNotNull(
|
||||||
|
file,
|
||||||
|
(this@allFiles as? MediaUploadInfo.Image)?.thumbnailFile,
|
||||||
|
(this@allFiles as? MediaUploadInfo.Video)?.thumbnailFile,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,13 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class FakeMediaPreProcessor : MediaPreProcessor {
|
class FakeMediaPreProcessor(
|
||||||
|
private val processLatch: CompletableDeferred<Unit>? = null,
|
||||||
|
) : MediaPreProcessor {
|
||||||
var processCallCount = 0
|
var processCallCount = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -41,6 +44,7 @@ class FakeMediaPreProcessor : MediaPreProcessor {
|
||||||
deleteOriginal: Boolean,
|
deleteOriginal: Boolean,
|
||||||
compressIfPossible: Boolean
|
compressIfPossible: Boolean
|
||||||
): Result<MediaUploadInfo> = simulateLongTask {
|
): Result<MediaUploadInfo> = simulateLongTask {
|
||||||
|
processLatch?.await()
|
||||||
processCallCount++
|
processCallCount++
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:361ff23fd2ff99aecfdffd10b45e9235d86183d4856cb2a3e99f85b9e04c2d59
|
oid sha256:3a92cb782d97553f364fab3df3fe159557a26aad8b7e5c176b40616c2f05abdd
|
||||||
size 51376
|
size 51410
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:efbfed755b29293009f45fca33f58863b612772de9a1d55593c979dbb04ff6f2
|
oid sha256:361ff23fd2ff99aecfdffd10b45e9235d86183d4856cb2a3e99f85b9e04c2d59
|
||||||
size 88981
|
size 51376
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:efbfed755b29293009f45fca33f58863b612772de9a1d55593c979dbb04ff6f2
|
||||||
|
size 88981
|
||||||
Loading…
Add table
Add a link
Reference in a new issue