Close the media preview screen ASAP with sending queue enabled (#4089)
* Close the attachment preview screen ASAP when sending media with the send queue is enabled * When the send queue FF is not enabled make sure to dismiss the screen after the media has been sent * Make sure we get a scaled thumbnail from videos too, not only for images * Unify several state holders into `SendActionState`. * Fix lint issues, add `Flow.firstInstanceOf` extension fun * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
6d1dac6d2d
commit
65ce91a8fb
15 changed files with 286 additions and 126 deletions
|
|
@ -12,6 +12,6 @@ import androidx.compose.runtime.Immutable
|
|||
@Immutable
|
||||
sealed interface AttachmentsPreviewEvents {
|
||||
data object SendAttachment : AttachmentsPreviewEvents
|
||||
data object Cancel : AttachmentsPreviewEvents
|
||||
data object ClearSendState : AttachmentsPreviewEvents
|
||||
data object CancelAndDismiss : AttachmentsPreviewEvents
|
||||
data object CancelAndClearSendState : AttachmentsPreviewEvents
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,18 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
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.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.coroutine.firstInstanceOf
|
||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
||||
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
|
||||
|
|
@ -48,6 +52,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
private val permalinkBuilder: PermalinkBuilder,
|
||||
private val temporaryUriDeleter: TemporaryUriDeleter,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : Presenter<AttachmentsPreviewState> {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
@ -72,71 +78,79 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
|
||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||
|
||||
val userSentAttachment = remember { mutableStateOf(false) }
|
||||
val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false)
|
||||
val showCaptionCompatibilityWarning by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning).collectAsState(initial = false)
|
||||
|
||||
val mediaUploadInfoState = remember { mutableStateOf<AsyncData<MediaUploadInfo>>(AsyncData.Uninitialized) }
|
||||
var useSendQueue by remember { mutableStateOf(false) }
|
||||
var preprocessMediaJob by remember { mutableStateOf<Job?>(null) }
|
||||
LaunchedEffect(Unit) {
|
||||
preProcessAttachment(
|
||||
useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
|
||||
preprocessMediaJob = preProcessAttachment(
|
||||
attachment,
|
||||
mediaUploadInfoState,
|
||||
sendActionState
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val observableSendState = snapshotFlow { sendActionState.value }
|
||||
|
||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||
when (attachmentsPreviewEvents) {
|
||||
is AttachmentsPreviewEvents.SendAttachment -> coroutineScope.launch {
|
||||
val useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||
userSentAttachment.value = true
|
||||
val instantSending = mediaUploadInfoState.value.isReady() && useSendQueue
|
||||
sendActionState.value = if (instantSending) {
|
||||
SendActionState.Sending.InstantSending
|
||||
} else {
|
||||
SendActionState.Sending.Processing
|
||||
is AttachmentsPreviewEvents.SendAttachment -> {
|
||||
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
||||
// If the processing was hidden before, make it visible now
|
||||
if (sendActionState.value is SendActionState.Sending.Processing) {
|
||||
sendActionState.value = SendActionState.Sending.Processing(displayProgress = true)
|
||||
}
|
||||
|
||||
// Wait until the media is ready to be uploaded
|
||||
val mediaUploadInfo = observableSendState.firstInstanceOf<SendActionState.Sending.ReadyToUpload>().mediaInfo
|
||||
|
||||
// Pre-processing is done, send the attachment
|
||||
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
|
||||
.takeIf { it.isNotEmpty() }
|
||||
|
||||
// If we're supposed to send the media as a background job, we can dismiss this screen already
|
||||
if (useSendQueue && coroutineContext.isActive) {
|
||||
onDoneListener()
|
||||
}
|
||||
|
||||
// If using the send queue, send it using the session coroutine scope so it doesn't matter if this screen or the chat one are closed
|
||||
val sendMediaCoroutineScope = if (useSendQueue) sessionCoroutineScope else coroutineScope
|
||||
sendMediaCoroutineScope.launch(dispatchers.io) {
|
||||
sendPreProcessedMedia(
|
||||
mediaUploadInfo = mediaUploadInfo,
|
||||
caption = caption,
|
||||
sendActionState = sendActionState,
|
||||
dismissAfterSend = !useSendQueue,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AttachmentsPreviewEvents.Cancel -> {
|
||||
coroutineScope.cancel(
|
||||
AttachmentsPreviewEvents.CancelAndDismiss -> {
|
||||
// Cancel media preprocessing and sending
|
||||
preprocessMediaJob?.cancel()
|
||||
ongoingSendAttachmentJob.value?.cancel()
|
||||
|
||||
// Dismiss the screen
|
||||
dismiss(
|
||||
attachment,
|
||||
mediaUploadInfoState.value,
|
||||
sendActionState,
|
||||
)
|
||||
}
|
||||
AttachmentsPreviewEvents.ClearSendState -> {
|
||||
AttachmentsPreviewEvents.CancelAndClearSendState -> {
|
||||
// Cancel media sending
|
||||
ongoingSendAttachmentJob.value?.let {
|
||||
it.cancel()
|
||||
ongoingSendAttachmentJob.value = null
|
||||
}
|
||||
sendActionState.value = SendActionState.Idle
|
||||
|
||||
val mediaUploadInfo = sendActionState.value.mediaUploadInfo()
|
||||
sendActionState.value = if (mediaUploadInfo != null) {
|
||||
SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
|
||||
} else {
|
||||
SendActionState.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,13 +167,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
|
||||
private fun CoroutineScope.preProcessAttachment(
|
||||
attachment: Attachment,
|
||||
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
||||
) = launch {
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
) = launch(dispatchers.io) {
|
||||
when (attachment) {
|
||||
is Attachment.Media -> {
|
||||
preProcessMedia(
|
||||
mediaAttachment = attachment,
|
||||
mediaUploadInfoState = mediaUploadInfoState,
|
||||
sendActionState = sendActionState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -167,37 +181,36 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
|
||||
private suspend fun preProcessMedia(
|
||||
mediaAttachment: Attachment.Media,
|
||||
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
) {
|
||||
mediaUploadInfoState.value = AsyncData.Loading()
|
||||
sendActionState.value = SendActionState.Sending.Processing(displayProgress = false)
|
||||
mediaSender.preProcessMedia(
|
||||
uri = mediaAttachment.localMedia.uri,
|
||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||
).fold(
|
||||
onSuccess = { mediaUploadInfo ->
|
||||
mediaUploadInfoState.value = AsyncData.Success(mediaUploadInfo)
|
||||
sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to pre-process media")
|
||||
if (it is CancellationException) {
|
||||
throw it
|
||||
} else {
|
||||
mediaUploadInfoState.value = AsyncData.Failure(it)
|
||||
sendActionState.value = SendActionState.Failure(it, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.cancel(
|
||||
private fun dismiss(
|
||||
attachment: Attachment,
|
||||
mediaUploadInfo: AsyncData<MediaUploadInfo>,
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
) = launch {
|
||||
) {
|
||||
// Delete the temporary file
|
||||
when (attachment) {
|
||||
is Attachment.Media -> {
|
||||
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
||||
mediaUploadInfo.dataOrNull()?.let { data ->
|
||||
sendActionState.value.mediaUploadInfo()?.let { data ->
|
||||
cleanUp(data)
|
||||
}
|
||||
}
|
||||
|
|
@ -219,13 +232,14 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
mediaUploadInfo: MediaUploadInfo,
|
||||
caption: String?,
|
||||
sendActionState: MutableState<SendActionState>,
|
||||
dismissAfterSend: Boolean,
|
||||
) = runCatching {
|
||||
val context = coroutineContext
|
||||
val progressCallback = object : ProgressCallback {
|
||||
override fun onProgress(current: Long, total: Long) {
|
||||
// Note will not happen if useSendQueue is true
|
||||
if (context.isActive) {
|
||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat(), mediaUploadInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -240,14 +254,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
|||
cleanUp(mediaUploadInfo)
|
||||
// Reset the sendActionState to ensure that dialog is closed before the screen
|
||||
sendActionState.value = SendActionState.Done
|
||||
onDoneListener()
|
||||
|
||||
if (dismissAfterSend) {
|
||||
onDoneListener()
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
Timber.e(error, "Failed to send attachment")
|
||||
if (error is CancellationException) {
|
||||
throw error
|
||||
} else {
|
||||
sendActionState.value = SendActionState.Failure(error)
|
||||
sendActionState.value = SendActionState.Failure(error, mediaUploadInfo)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.attachments.preview
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
|
||||
data class AttachmentsPreviewState(
|
||||
|
|
@ -26,11 +27,18 @@ sealed interface SendActionState {
|
|||
|
||||
@Immutable
|
||||
sealed interface Sending : SendActionState {
|
||||
data object InstantSending : Sending
|
||||
data object Processing : Sending
|
||||
data class Uploading(val progress: Float) : Sending
|
||||
data class Processing(val displayProgress: Boolean) : Sending
|
||||
data class ReadyToUpload(val mediaInfo: MediaUploadInfo) : Sending
|
||||
data class Uploading(val progress: Float, val mediaUploadInfo: MediaUploadInfo) : Sending
|
||||
}
|
||||
|
||||
data class Failure(val error: Throwable) : SendActionState
|
||||
data class Failure(val error: Throwable, val mediaUploadInfo: MediaUploadInfo?) : SendActionState
|
||||
data object Done : SendActionState
|
||||
|
||||
fun mediaUploadInfo(): MediaUploadInfo? = when (this) {
|
||||
is Sending.ReadyToUpload -> mediaInfo
|
||||
is Sending.Uploading -> mediaUploadInfo
|
||||
is Failure -> mediaUploadInfo
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,25 @@ package io.element.android.features.messages.impl.attachments.preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.core.net.toUri
|
||||
import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.media.ImageInfo
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaviewer.api.MediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||
import java.io.File
|
||||
|
||||
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
||||
override val values: Sequence<AttachmentsPreviewState>
|
||||
get() = sequenceOf(
|
||||
anAttachmentsPreviewState(),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = false)),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = true)),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.ReadyToUpload(aMediaUploadInfo())),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f, aMediaUploadInfo())),
|
||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"), aMediaUploadInfo())),
|
||||
anAttachmentsPreviewState(allowCaption = false),
|
||||
anAttachmentsPreviewState(showCaptionCompatibilityWarning = true),
|
||||
)
|
||||
|
|
@ -44,3 +50,20 @@ fun anAttachmentsPreviewState(
|
|||
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
|
||||
eventSink = {}
|
||||
)
|
||||
|
||||
fun aMediaUploadInfo(
|
||||
filePath: String = "file://path",
|
||||
thumbnailFilePath: String? = null,
|
||||
) = MediaUploadInfo.Image(
|
||||
file = File(filePath),
|
||||
imageInfo = ImageInfo(
|
||||
height = 100,
|
||||
width = 100,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 1000,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
),
|
||||
thumbnailFile = thumbnailFilePath?.let { File(it) },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,11 +59,11 @@ fun AttachmentsPreviewView(
|
|||
}
|
||||
|
||||
fun postCancel() {
|
||||
state.eventSink(AttachmentsPreviewEvents.Cancel)
|
||||
state.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
|
||||
}
|
||||
|
||||
fun postClearSendState() {
|
||||
state.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||
state.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||
}
|
||||
|
||||
BackHandler(enabled = state.sendActionState !is SendActionState.Sending) {
|
||||
|
|
@ -106,12 +106,14 @@ private fun AttachmentSendStateView(
|
|||
) {
|
||||
when (sendActionState) {
|
||||
is SendActionState.Sending.Processing -> {
|
||||
ProgressDialog(
|
||||
type = ProgressDialogType.Indeterminate,
|
||||
text = stringResource(id = CommonStrings.common_sending),
|
||||
showCancelButton = true,
|
||||
onDismissRequest = onDismissClick,
|
||||
)
|
||||
if (sendActionState.displayProgress) {
|
||||
ProgressDialog(
|
||||
type = ProgressDialogType.Indeterminate,
|
||||
text = stringResource(id = CommonStrings.common_sending),
|
||||
showCancelButton = true,
|
||||
onDismissRequest = onDismissClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
is SendActionState.Sending.Uploading -> {
|
||||
ProgressDialog(
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ 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.fixtures.aMediaAttachment
|
||||
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
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
|
||||
|
|
@ -35,20 +36,24 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
|||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
||||
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.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
|
|
@ -121,13 +126,14 @@ class AttachmentsPreviewPresenterTest {
|
|||
}.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)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f, mediaUploadInfo))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f, mediaUploadInfo))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f, mediaUploadInfo))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
onDoneListener.assertions().isCalledOnce()
|
||||
|
|
@ -159,10 +165,10 @@ class AttachmentsPreviewPresenterTest {
|
|||
// Pre-processing finishes
|
||||
processLatch.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||
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.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
onDoneListener.assertions().isCalledOnce()
|
||||
|
|
@ -191,12 +197,13 @@ class AttachmentsPreviewPresenterTest {
|
|||
}.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(displayProgress = false))
|
||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
// Pre-processing finishes
|
||||
processLatch.complete(Unit)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
onDoneListener.assertions().isCalledOnce()
|
||||
|
|
@ -222,8 +229,7 @@ class AttachmentsPreviewPresenterTest {
|
|||
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)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||
// Pre-processing finishes
|
||||
processLatch.complete(Unit)
|
||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||
|
|
@ -252,8 +258,7 @@ class AttachmentsPreviewPresenterTest {
|
|||
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.Sending.Processing(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -271,7 +276,7 @@ class AttachmentsPreviewPresenterTest {
|
|||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.Cancel)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
deleteCallback.assertions().isCalledOnce()
|
||||
|
|
@ -305,8 +310,8 @@ class AttachmentsPreviewPresenterTest {
|
|||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||
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(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
sendImageResult.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
|
|
@ -346,8 +351,8 @@ class AttachmentsPreviewPresenterTest {
|
|||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||
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(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
sendVideoResult.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
|
|
@ -385,8 +390,8 @@ class AttachmentsPreviewPresenterTest {
|
|||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||
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(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||
sendAudioResult.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
|
|
@ -400,7 +405,7 @@ class AttachmentsPreviewPresenterTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `present - send media failure scenario`() = runTest {
|
||||
fun `present - send media failure scenario without media queue`() = runTest {
|
||||
val failure = MediaPreProcessor.Failure(null)
|
||||
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||
Result.failure(failure)
|
||||
|
|
@ -408,7 +413,7 @@ class AttachmentsPreviewPresenterTest {
|
|||
val room = FakeMatrixRoom(
|
||||
sendFileResult = sendFileResult,
|
||||
)
|
||||
val presenter = createAttachmentsPreviewPresenter(room = room)
|
||||
val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = false)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -416,20 +421,28 @@ class AttachmentsPreviewPresenterTest {
|
|||
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)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
|
||||
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure, mediaUploadInfo))
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||
val clearedState = awaitItem()
|
||||
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
failureState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||
val clearedState = awaitLastSequentialItem()
|
||||
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - dismissing the progress dialog stops media upload`() = runTest {
|
||||
val presenter = createAttachmentsPreviewPresenter()
|
||||
fun `present - send media failure scenario with media queue`() = runTest {
|
||||
val failure = MediaPreProcessor.Failure(null)
|
||||
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||
Result.failure(failure)
|
||||
}
|
||||
val onDoneListenerResult = lambdaRecorder<Unit> {}
|
||||
val room = FakeMatrixRoom(
|
||||
sendFileResult = sendFileResult,
|
||||
)
|
||||
val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
|
|
@ -437,14 +450,62 @@ class AttachmentsPreviewPresenterTest {
|
|||
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)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
|
||||
// Check that the onDoneListener is called so the screen would be dismissed
|
||||
onDoneListenerResult.assertions().isCalledOnce()
|
||||
|
||||
val failureState = awaitItem()
|
||||
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure, mediaUploadInfo))
|
||||
sendFileResult.assertions().isCalledOnce()
|
||||
failureState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||
val clearedState = awaitLastSequentialItem()
|
||||
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAttachmentsPreviewPresenter(
|
||||
@Test
|
||||
fun `present - dismissing the progress dialog stops media upload without media queue`() = runTest {
|
||||
val presenter = createAttachmentsPreviewPresenter(mediaUploadOnSendQueueEnabled = false)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
initialState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||
// The sending is cancelled and the state is kept at ReadyToUpload
|
||||
ensureAllEventsConsumed()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - dismissing the progress dialog stops media upload with media queue`() = runTest {
|
||||
val onDoneListenerResult = lambdaRecorder<Unit> {}
|
||||
val presenter = createAttachmentsPreviewPresenter(mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult)
|
||||
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(displayProgress = false))
|
||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||
initialState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||
// The sending is cancelled and the state is kept at ReadyToUpload
|
||||
ensureAllEventsConsumed()
|
||||
|
||||
// Check that the onDoneListener is called so the screen would be dismissed
|
||||
onDoneListenerResult.assertions().isCalledOnce()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createAttachmentsPreviewPresenter(
|
||||
localMedia: LocalMedia = aLocalMedia(
|
||||
uri = mockMediaUrl,
|
||||
),
|
||||
|
|
@ -469,7 +530,19 @@ class AttachmentsPreviewPresenterTest {
|
|||
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
||||
FeatureFlags.MediaCaptionWarning.key to showCaptionCompatibilityWarning,
|
||||
),
|
||||
)
|
||||
),
|
||||
sessionCoroutineScope = this,
|
||||
dispatchers = testCoroutineDispatchers(),
|
||||
)
|
||||
}
|
||||
|
||||
private val mediaUploadInfo = MediaUploadInfo.AnyFile(
|
||||
File("test"),
|
||||
FileInfo(
|
||||
mimetype = MimeTypes.Any,
|
||||
size = 999L,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.core.coroutine
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the first element of the flow that is an instance of [T], waiting for it if necessary.
|
||||
*/
|
||||
suspend inline fun <reified T> Flow<*>.firstInstanceOf(): T {
|
||||
return first { it is T } as T
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.MediaMetadataRetriever.OPTION_CLOSEST_SYNC
|
||||
import android.media.ThumbnailUtils
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
|
|
@ -18,6 +19,7 @@ import android.provider.MediaStore
|
|||
import android.util.Size
|
||||
import androidx.core.net.toUri
|
||||
import com.vanniktech.blurhash.BlurHash
|
||||
import io.element.android.libraries.androidutils.bitmap.resizeToMax
|
||||
import io.element.android.libraries.androidutils.file.createTmpFile
|
||||
import io.element.android.libraries.androidutils.media.runAndRelease
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
|
@ -89,7 +91,11 @@ class ThumbnailFactory @Inject constructor(
|
|||
return createThumbnail(mimeType = MimeTypes.Jpeg) {
|
||||
MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, file.toUri())
|
||||
getFrameAtTime(VIDEO_THUMB_FRAME)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
getScaledFrameAtTime(VIDEO_THUMB_FRAME, OPTION_CLOSEST_SYNC, THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT)
|
||||
} else {
|
||||
getFrameAtTime(VIDEO_THUMB_FRAME)?.resizeToMax(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:26764da6595dee37219d5bbf3e53bad8db6451d7d39fa7741c61d96c89938bf8
|
||||
size 51246
|
||||
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||
size 395102
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b9d5ee3eb77075ca147fc7ed96c83a846dcfcf8e09bc129a24daa5ed19f262a7
|
||||
size 51217
|
||||
oid sha256:26764da6595dee37219d5bbf3e53bad8db6451d7d39fa7741c61d96c89938bf8
|
||||
size 51246
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9f2882937b7dd90da5d1708185f0130b75659eed57eaf2a966280a891f4ddbd6
|
||||
size 89083
|
||||
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||
size 395102
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7af7dedec8de4ac367fc870d2c235060b3a57d27b2bb879dc784a3cef3c2a370
|
||||
size 390628
|
||||
oid sha256:b9d5ee3eb77075ca147fc7ed96c83a846dcfcf8e09bc129a24daa5ed19f262a7
|
||||
size 51217
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||
size 395102
|
||||
oid sha256:9f2882937b7dd90da5d1708185f0130b75659eed57eaf2a966280a891f4ddbd6
|
||||
size 89083
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7af7dedec8de4ac367fc870d2c235060b3a57d27b2bb879dc784a3cef3c2a370
|
||||
size 390628
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||
size 395102
|
||||
Loading…
Add table
Add a link
Reference in a new issue