diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 0ea9caf29c..1582207a1c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -65,6 +65,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.ProgressDialogType import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.button.BackButton @@ -196,7 +197,15 @@ private fun AttachmentStateView( is AttachmentsState.Previewing -> LaunchedEffect(state) { onPreviewAttachments(state.attachments) } - is AttachmentsState.Sending -> ProgressDialog(text = stringResource(id = CommonStrings.common_loading)) + is AttachmentsState.Sending -> { + ProgressDialog( + type = when (state) { + is AttachmentsState.Sending.Uploading -> ProgressDialogType.Determinate(state.progress) + is AttachmentsState.Sending.Processing -> ProgressDialogType.Indeterminate + }, + text = stringResource(id = CommonStrings.common_sending) + ) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index 9761a939b3..12ca7ca3f0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -25,9 +25,8 @@ 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.architecture.Async import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.runUpdatingState +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.mediaupload.api.MediaSender import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -48,13 +47,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( val coroutineScope = rememberCoroutineScope() val sendActionState = remember { - mutableStateOf>(Async.Uninitialized) + mutableStateOf(SendActionState.Idle) } fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) { when (attachmentsPreviewEvents) { AttachmentsPreviewEvents.SendAttachment -> coroutineScope.sendAttachment(attachment, sendActionState) - AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = Async.Uninitialized + AttachmentsPreviewEvents.ClearSendState -> sendActionState.value = SendActionState.Idle } } @@ -67,7 +66,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( private fun CoroutineScope.sendAttachment( attachment: Attachment, - sendActionState: MutableState>, + sendActionState: MutableState, ) = launch { when (attachment) { is Attachment.Media -> { @@ -81,10 +80,26 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( private suspend fun sendMedia( mediaAttachment: Attachment.Media, - sendActionState: MutableState>, + sendActionState: MutableState, ) { - sendActionState.runUpdatingState { - mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible) + val progressCallback = object : ProgressCallback { + override fun onProgress(current: Long, total: Long) { + sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total) + } } + sendActionState.value = SendActionState.Sending.Processing + mediaSender.sendMedia( + uri = mediaAttachment.localMedia.uri, + mimeType = mediaAttachment.localMedia.info.mimeType, + compressIfPossible = mediaAttachment.compressIfPossible, + progressCallback = progressCallback + ).fold( + onSuccess = { + sendActionState.value = SendActionState.Done + }, + onFailure = { + sendActionState.value = SendActionState.Failure(it) + } + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt index 67350f5048..e41f43040f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewState.kt @@ -17,10 +17,21 @@ package io.element.android.features.messages.impl.attachments.preview import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.libraries.architecture.Async data class AttachmentsPreviewState( val attachment: Attachment, - val sendActionState: Async, + val sendActionState: SendActionState, val eventSink: (AttachmentsPreviewEvents) -> Unit ) + +sealed interface SendActionState { + object Idle : SendActionState + sealed interface Sending : SendActionState { + object Processing : Sending + data class Uploading(val progress: Float) : Sending + } + + data class Failure(val error: Throwable) : SendActionState + object Done : SendActionState +} + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 58fea4a4f2..ee41ace4b0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -22,23 +22,21 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.local.aFileInfo -import io.element.android.features.messages.impl.media.local.aVideoInfo import io.element.android.features.messages.impl.media.local.anImageInfo -import io.element.android.libraries.architecture.Async open class AttachmentsPreviewStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( anAttachmentsPreviewState(), anAttachmentsPreviewState(mediaInfo = aFileInfo()), - anAttachmentsPreviewState(sendActionState = Async.Loading()), - anAttachmentsPreviewState(sendActionState = Async.Failure(RuntimeException())), + anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)), + anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException())), ) } fun anAttachmentsPreviewState( mediaInfo: MediaInfo = anImageInfo(), - sendActionState: Async = Async.Uninitialized) = AttachmentsPreviewState( + sendActionState: SendActionState = SendActionState.Idle) = AttachmentsPreviewState( attachment = Attachment.Media( localMedia = LocalMedia("file://path".toUri(), mediaInfo), compressIfPossible = true diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt index 7388dd665b..6f33ef5d0b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewView.kt @@ -33,9 +33,9 @@ import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaView -import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.ProgressDialogType import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.theme.components.Scaffold @@ -58,7 +58,7 @@ fun AttachmentsPreviewView( state.eventSink(AttachmentsPreviewEvents.ClearSendState) } - if (state.sendActionState is Async.Success) { + if (state.sendActionState is SendActionState.Done) { LaunchedEffect(state.sendActionState) { onDismiss() } @@ -78,26 +78,32 @@ fun AttachmentsPreviewView( } AttachmentSendStateView( sendActionState = state.sendActionState, - onRetryClicked = ::postSendAttachment, - onRetryDismissed = ::postClearSendState + onDismissClicked = ::postClearSendState, + onRetryClicked = ::postSendAttachment ) } @Composable private fun AttachmentSendStateView( - sendActionState: Async, - onRetryDismissed: () -> Unit, + sendActionState: SendActionState, + onDismissClicked: () -> Unit, onRetryClicked: () -> Unit ) { - when (sendActionState) { - is Async.Loading -> { - ProgressDialog(text = stringResource(id = CommonStrings.common_loading)) - } - is Async.Failure -> { + when (sendActionState) { + is SendActionState.Sending -> { + ProgressDialog( + type = when (sendActionState) { + is SendActionState.Sending.Uploading -> ProgressDialogType.Determinate(sendActionState.progress) + SendActionState.Sending.Processing -> ProgressDialogType.Indeterminate + }, + text = stringResource(id = CommonStrings.common_sending) + ) + } + is SendActionState.Failure -> { RetryDialog( content = stringResource(sendAttachmentError(sendActionState.error)), - onDismiss = onRetryDismissed, + onDismiss = onDismissClicked, onRetry = onRetryClicked ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 39b003e0df..586977942b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -42,6 +42,7 @@ import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn 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.room.MatrixRoom import io.element.android.libraries.mediapickers.api.PickerProvider import io.element.android.libraries.mediaupload.api.MediaSender @@ -107,7 +108,7 @@ class MessageComposerPresenter @Inject constructor( LaunchedEffect(attachmentsState.value) { when (val attachmentStateValue = attachmentsState.value) { - is AttachmentsState.Sending -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState) + is AttachmentsState.Sending.Processing -> localCoroutineScope.sendAttachment(attachmentStateValue.attachments.first(), attachmentsState) else -> Unit } } @@ -238,7 +239,7 @@ class MessageComposerPresenter @Inject constructor( attachmentsState.value = if (isPreviewable) { AttachmentsState.Previewing(persistentListOf(mediaAttachment)) } else { - AttachmentsState.Sending(persistentListOf(mediaAttachment)) + AttachmentsState.Sending.Processing(persistentListOf(mediaAttachment)) } } @@ -247,7 +248,12 @@ class MessageComposerPresenter @Inject constructor( mimeType: String, attachmentState: MutableState, ) { - mediaSender.sendMedia(uri, mimeType, compressIfPossible = false) + val progressCallback = object : ProgressCallback { + override fun onProgress(current: Long, total: Long) { + attachmentState.value = AttachmentsState.Sending.Uploading(current.toFloat() / total) + } + } + mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback) .onSuccess { attachmentState.value = AttachmentsState.None }.onFailure { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 9c6a2c650a..fb4c3e82ba 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -38,5 +38,8 @@ data class MessageComposerState( sealed interface AttachmentsState { object None : AttachmentsState data class Previewing(val attachments: ImmutableList) : AttachmentsState - data class Sending(val attachments: ImmutableList) : AttachmentsState + sealed interface Sending : AttachmentsState { + data class Processing(val attachments: ImmutableList) : Sending + data class Uploading(val progress: Float) : Sending + } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt index 35e6743881..99c2c2eacd 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt @@ -37,10 +37,16 @@ import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text +sealed interface ProgressDialogType { + data class Determinate(val progress: Float) : ProgressDialogType + object Indeterminate : ProgressDialogType +} + @Composable fun ProgressDialog( modifier: Modifier = Modifier, text: String? = null, + type: ProgressDialogType = ProgressDialogType.Indeterminate, onDismiss: () -> Unit = {}, ) { Dialog( @@ -50,6 +56,21 @@ fun ProgressDialog( ProgressDialogContent( modifier = modifier, text = text, + progressIndicator = { + when (type) { + is ProgressDialogType.Indeterminate -> { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary + ) + } + is ProgressDialogType.Determinate -> { + CircularProgressIndicator( + progress = type.progress, + color = MaterialTheme.colorScheme.primary + ) + } + } + } ) } } @@ -58,6 +79,11 @@ fun ProgressDialog( private fun ProgressDialogContent( modifier: Modifier = Modifier, text: String? = null, + progressIndicator: @Composable () -> Unit = { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.primary + ) + } ) { Box( contentAlignment = Alignment.Center, @@ -71,9 +97,7 @@ private fun ProgressDialogContent( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp) ) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary - ) + progressIndicator() if (!text.isNullOrBlank()) { Spacer(modifier = Modifier.height(22.dp)) Text( diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 9f27824858..6dab564b6e 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.mediaupload.api import android.net.Uri import io.element.android.libraries.core.extensions.flatMap +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.room.MatrixRoom import javax.inject.Inject @@ -26,7 +27,12 @@ class MediaSender @Inject constructor( private val room: MatrixRoom, ) { - suspend fun sendMedia(uri: Uri, mimeType: String, compressIfPossible: Boolean): Result { + suspend fun sendMedia( + uri: Uri, + mimeType: String, + compressIfPossible: Boolean, + progressCallback: ProgressCallback? = null + ): Result { return preProcessor .process( uri = uri, @@ -35,12 +41,13 @@ class MediaSender @Inject constructor( compressIfPossible = compressIfPossible ) .flatMap { info -> - room.sendMedia(info) + room.sendMedia(info, progressCallback) } } private suspend fun MatrixRoom.sendMedia( info: MediaUploadInfo, + progressCallback: ProgressCallback? ): Result { return when (info) { is MediaUploadInfo.Image -> { @@ -48,7 +55,7 @@ class MediaSender @Inject constructor( file = info.file, thumbnailFile = info.thumbnailFile, imageInfo = info.info, - progressCallback = null + progressCallback = progressCallback ) } @@ -57,7 +64,7 @@ class MediaSender @Inject constructor( file = info.file, thumbnailFile = info.thumbnailFile, videoInfo = info.info, - progressCallback = null + progressCallback = progressCallback ) } @@ -65,7 +72,7 @@ class MediaSender @Inject constructor( sendFile( file = info.file, fileInfo = info.info, - progressCallback = null + progressCallback = progressCallback ) } else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info"))