Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close the media preview screen ASAP with sending queue enabled #4089

Merged
merged 8 commits into from
Jan 8, 2025
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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to invoke this in the case of sendOnBackground is false, i.e. FeatureFlags.MediaUploadOnSendQueue is disabled.

Confirmed by test: the screen does not close


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
Loading