Merge pull request #3943 from element-hq/feature/bma/_poc/mediaPreprocessing

Hide media preprocessing
This commit is contained in:
Benoit Marty 2024-11-26 16:42:42 +01:00 committed by GitHub
commit b2a79d2dc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 325 additions and 38 deletions

View file

@ -8,6 +8,7 @@
package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -19,10 +20,16 @@ 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.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.permalink.PermalinkBuilder
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.rememberMarkdownTextEditorState
import kotlinx.coroutines.CancellationException
@ -39,6 +46,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
private val mediaSender: MediaSender,
private val permalinkBuilder: PermalinkBuilder,
private val temporaryUriDeleter: TemporaryUriDeleter,
private val featureFlagsService: FeatureFlagService,
) : Presenter<AttachmentsPreviewState> {
@AssistedFactory
interface Factory {
@ -63,19 +71,62 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
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) {
when (attachmentsPreviewEvents) {
is AttachmentsPreviewEvents.SendAttachment -> {
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
.takeIf { it.isNotEmpty() }
ongoingSendAttachmentJob.value = coroutineScope.sendAttachment(
attachment = attachment,
caption = caption,
sendActionState = sendActionState,
)
is AttachmentsPreviewEvents.SendAttachment -> coroutineScope.launch {
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
userSentAttachment.value = true
val instantSending = mediaUploadInfoState.value.isReady() && useSendQueue
sendActionState.value = if (instantSending) {
SendActionState.Sending.InstantSending
} else {
SendActionState.Sending.Processing
}
}
AttachmentsPreviewEvents.Cancel -> {
coroutineScope.cancel(attachment)
coroutineScope.cancel(
attachment,
mediaUploadInfoState.value,
sendActionState,
)
}
AttachmentsPreviewEvents.ClearSendState -> {
ongoingSendAttachmentJob.value?.let {
@ -95,56 +146,95 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
)
}
private fun CoroutineScope.sendAttachment(
private fun CoroutineScope.preProcessAttachment(
attachment: Attachment,
caption: String?,
sendActionState: MutableState<SendActionState>,
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
) = launch {
when (attachment) {
is Attachment.Media -> {
sendMedia(
preProcessMedia(
mediaAttachment = attachment,
caption = caption,
sendActionState = sendActionState,
mediaUploadInfoState = mediaUploadInfoState,
)
}
}
}
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(
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 ->
cleanUp(data)
}
}
}
// Reset the sendActionState to ensure that dialog is closed before the screen
sendActionState.value = SendActionState.Done
onDoneListener()
}
private suspend fun sendMedia(
mediaAttachment: Attachment.Media,
private fun cleanUp(
mediaUploadInfo: MediaUploadInfo,
) {
mediaUploadInfo.allFiles().forEach { file ->
file.safeDelete()
}
}
private suspend fun sendPreProcessedMedia(
mediaUploadInfo: MediaUploadInfo,
caption: String?,
sendActionState: MutableState<SendActionState>,
) = 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.Processing
mediaSender.sendMedia(
uri = mediaAttachment.localMedia.uri,
mimeType = mediaAttachment.localMedia.info.mimeType,
mediaSender.sendPreProcessedMedia(
mediaUploadInfo = mediaUploadInfo,
caption = caption,
formattedCaption = null,
progressCallback = progressCallback
).getOrThrow()
}.fold(
onSuccess = {
cleanUp(mediaUploadInfo)
// Reset the sendActionState to ensure that dialog is closed before the screen
sendActionState.value = SendActionState.Done
onDoneListener()
},
onFailure = { error ->

View file

@ -27,9 +27,11 @@ 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 Failure(val error: Throwable) : SendActionState
data object Done : SendActionState
}

View file

@ -26,6 +26,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
anAttachmentsPreviewState(mediaInfo = aVideoMediaInfo()),
anAttachmentsPreviewState(mediaInfo = anAudioMediaInfo()),
anAttachmentsPreviewState(mediaInfo = anApkMediaInfo()),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
)

View file

@ -99,12 +99,17 @@ private fun AttachmentSendStateView(
onRetryClick: () -> Unit
) {
when (sendActionState) {
is SendActionState.Sending -> {
is SendActionState.Sending.Processing -> {
ProgressDialog(
type = when (sendActionState) {
is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress)
SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate
},
type = ProgressDialogType.Indeterminate,
text = stringResource(id = CommonStrings.common_sending),
showCancelButton = true,
onDismissRequest = onDismissClick,
)
}
is SendActionState.Sending.Uploading -> {
ProgressDialog(
type = ProgressDialogType.Determinate(sendActionState.progress),
text = stringResource(id = CommonStrings.common_sending),
showCancelButton = true,
onDismissRequest = onDismissClick,