Skip to content

Commit

Permalink
Close the media preview screen ASAP with sending queue enabled (eleme…
Browse files Browse the repository at this point in the history
…nt-hq#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 <[email protected]>
  • Loading branch information
jmartinesp and ElementBot authored Jan 8, 2025
1 parent 6d1dac6 commit 65ce91a
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 119 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 -> {
val observableSendState = snapshotFlow { sendActionState.value }

fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
when (attachmentsPreviewEvents) {
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() }
ongoingSendAttachmentJob.value = coroutineScope.launch {

// 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.data,
mediaUploadInfo = mediaUploadInfo,
caption = caption,
sendActionState = sendActionState,
dismissAfterSend = !useSendQueue,
)
}
}
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
}
}
}
}
AttachmentsPreviewEvents.CancelAndDismiss -> {
// Cancel media preprocessing and sending
preprocessMediaJob?.cancel()
ongoingSendAttachmentJob.value?.cancel()

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
}
}
AttachmentsPreviewEvents.Cancel -> {
coroutineScope.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
}
}
}
}
Expand All @@ -153,51 +167,50 @@ 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,
)
}
}
}

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)
}
}
Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)
Expand All @@ -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) },
)
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit 65ce91a

Please sign in to comment.