Merge pull request #721 from vector-im/feature/fga/media_upload_progress
Feature/fga/media upload progress
This commit is contained in:
commit
e4aae094c2
13 changed files with 170 additions and 58 deletions
|
|
@ -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.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||||
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
import io.element.android.libraries.androidutils.ui.hideKeyboard
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
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.Avatar
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
|
@ -216,7 +217,15 @@ private fun AttachmentStateView(
|
||||||
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
is AttachmentsState.Previewing -> LaunchedEffect(state) {
|
||||||
onPreviewAttachments(state.attachments)
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,8 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
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.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 io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -48,13 +47,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
val sendActionState = remember {
|
val sendActionState = remember {
|
||||||
mutableStateOf<Async<Unit>>(Async.Uninitialized)
|
mutableStateOf<SendActionState>(SendActionState.Idle)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||||
when (attachmentsPreviewEvents) {
|
when (attachmentsPreviewEvents) {
|
||||||
AttachmentsPreviewEvents.SendAttachment -> coroutineScope.sendAttachment(attachment, sendActionState)
|
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(
|
private fun CoroutineScope.sendAttachment(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
sendActionState: MutableState<Async<Unit>>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) = launch {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
|
|
@ -81,10 +80,26 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private suspend fun sendMedia(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaAttachment: Attachment.Media,
|
||||||
sendActionState: MutableState<Async<Unit>>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) {
|
) {
|
||||||
sendActionState.runUpdatingState {
|
val progressCallback = object : ProgressCallback {
|
||||||
mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible)
|
override fun onProgress(current: Long, total: Long) {
|
||||||
|
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,
|
||||||
|
compressIfPossible = mediaAttachment.compressIfPossible,
|
||||||
|
progressCallback = progressCallback
|
||||||
|
).fold(
|
||||||
|
onSuccess = {
|
||||||
|
sendActionState.value = SendActionState.Done
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
sendActionState.value = SendActionState.Failure(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,21 @@
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.libraries.architecture.Async
|
|
||||||
|
|
||||||
data class AttachmentsPreviewState(
|
data class AttachmentsPreviewState(
|
||||||
val attachment: Attachment,
|
val attachment: Attachment,
|
||||||
val sendActionState: Async<Unit>,
|
val sendActionState: SendActionState,
|
||||||
val eventSink: (AttachmentsPreviewEvents) -> Unit
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.LocalMedia
|
||||||
import io.element.android.features.messages.impl.media.local.MediaInfo
|
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.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.features.messages.impl.media.local.anImageInfo
|
||||||
import io.element.android.libraries.architecture.Async
|
|
||||||
|
|
||||||
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
|
||||||
override val values: Sequence<AttachmentsPreviewState>
|
override val values: Sequence<AttachmentsPreviewState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
anAttachmentsPreviewState(),
|
anAttachmentsPreviewState(),
|
||||||
anAttachmentsPreviewState(mediaInfo = aFileInfo()),
|
anAttachmentsPreviewState(mediaInfo = aFileInfo()),
|
||||||
anAttachmentsPreviewState(sendActionState = Async.Loading()),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
||||||
anAttachmentsPreviewState(sendActionState = Async.Failure(RuntimeException())),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun anAttachmentsPreviewState(
|
fun anAttachmentsPreviewState(
|
||||||
mediaInfo: MediaInfo = anImageInfo(),
|
mediaInfo: MediaInfo = anImageInfo(),
|
||||||
sendActionState: Async<Unit> = Async.Uninitialized) = AttachmentsPreviewState(
|
sendActionState: SendActionState = SendActionState.Idle) = AttachmentsPreviewState(
|
||||||
attachment = Attachment.Media(
|
attachment = Attachment.Media(
|
||||||
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
|
||||||
compressIfPossible = true
|
compressIfPossible = true
|
||||||
|
|
|
||||||
|
|
@ -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.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError
|
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.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.atomic.molecules.ButtonRowMolecule
|
||||||
import io.element.android.libraries.designsystem.components.ProgressDialog
|
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.components.dialogs.RetryDialog
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
|
@ -58,7 +58,7 @@ fun AttachmentsPreviewView(
|
||||||
state.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
state.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.sendActionState is Async.Success) {
|
if (state.sendActionState is SendActionState.Done) {
|
||||||
LaunchedEffect(state.sendActionState) {
|
LaunchedEffect(state.sendActionState) {
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
|
|
@ -78,26 +78,32 @@ fun AttachmentsPreviewView(
|
||||||
}
|
}
|
||||||
AttachmentSendStateView(
|
AttachmentSendStateView(
|
||||||
sendActionState = state.sendActionState,
|
sendActionState = state.sendActionState,
|
||||||
onRetryClicked = ::postSendAttachment,
|
onDismissClicked = ::postClearSendState,
|
||||||
onRetryDismissed = ::postClearSendState
|
onRetryClicked = ::postSendAttachment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AttachmentSendStateView(
|
private fun AttachmentSendStateView(
|
||||||
sendActionState: Async<Unit>,
|
sendActionState: SendActionState,
|
||||||
onRetryDismissed: () -> Unit,
|
onDismissClicked: () -> Unit,
|
||||||
onRetryClicked: () -> 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(
|
RetryDialog(
|
||||||
content = stringResource(sendAttachmentError(sendActionState.error)),
|
content = stringResource(sendAttachmentError(sendActionState.error)),
|
||||||
onDismiss = onRetryDismissed,
|
onDismiss = onDismissClicked,
|
||||||
onRetry = onRetryClicked
|
onRetry = onRetryClicked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import io.element.android.libraries.di.RoomScope
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
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.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.mediapickers.api.PickerProvider
|
import io.element.android.libraries.mediapickers.api.PickerProvider
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
|
@ -110,7 +111,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
|
|
||||||
LaunchedEffect(attachmentsState.value) {
|
LaunchedEffect(attachmentsState.value) {
|
||||||
when (val attachmentStateValue = 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
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -245,7 +246,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
attachmentsState.value = if (isPreviewable) {
|
attachmentsState.value = if (isPreviewable) {
|
||||||
AttachmentsState.Previewing(persistentListOf(mediaAttachment))
|
AttachmentsState.Previewing(persistentListOf(mediaAttachment))
|
||||||
} else {
|
} else {
|
||||||
AttachmentsState.Sending(persistentListOf(mediaAttachment))
|
AttachmentsState.Sending.Processing(persistentListOf(mediaAttachment))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +255,12 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
attachmentState: MutableState<AttachmentsState>,
|
attachmentState: MutableState<AttachmentsState>,
|
||||||
) {
|
) {
|
||||||
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.toFloat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false, progressCallback)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
attachmentState.value = AttachmentsState.None
|
attachmentState.value = AttachmentsState.None
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|
|
||||||
|
|
@ -39,5 +39,8 @@ data class MessageComposerState(
|
||||||
sealed interface AttachmentsState {
|
sealed interface AttachmentsState {
|
||||||
object None : AttachmentsState
|
object None : AttachmentsState
|
||||||
data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState
|
data class Previewing(val attachments: ImmutableList<Attachment>) : AttachmentsState
|
||||||
data class Sending(val attachments: ImmutableList<Attachment>) : AttachmentsState
|
sealed interface Sending : AttachmentsState {
|
||||||
|
data class Processing(val attachments: ImmutableList<Attachment>) : Sending
|
||||||
|
data class Uploading(val progress: Float) : Sending
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,8 @@ import io.element.android.features.messages.fixtures.aLocalMedia
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
|
||||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
|
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
|
||||||
|
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||||
import io.element.android.features.messages.impl.media.local.LocalMedia
|
import io.element.android.features.messages.impl.media.local.LocalMedia
|
||||||
import io.element.android.libraries.architecture.Async
|
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
|
|
@ -47,17 +47,26 @@ class AttachmentsPreviewPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - send media success scenario`() = runTest {
|
fun `present - send media success scenario`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
|
room.givenProgressCallbackValues(
|
||||||
|
listOf(
|
||||||
|
Pair(0, 10),
|
||||||
|
Pair(5, 10),
|
||||||
|
Pair(10, 10)
|
||||||
|
)
|
||||||
|
)
|
||||||
val presenter = anAttachmentsPreviewPresenter(room = room)
|
val presenter = anAttachmentsPreviewPresenter(room = room)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
val loadingState = awaitItem()
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
assertThat(loadingState.sendActionState).isEqualTo(Async.Loading<Unit>())
|
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))
|
||||||
val successState = awaitItem()
|
val successState = awaitItem()
|
||||||
assertThat(successState.sendActionState).isEqualTo(Async.Success(Unit))
|
assertThat(successState.sendActionState).isEqualTo(SendActionState.Done)
|
||||||
assertThat(room.sendMediaCount).isEqualTo(1)
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,16 +81,16 @@ class AttachmentsPreviewPresenterTest {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(Async.Uninitialized)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
val loadingState = awaitItem()
|
val loadingState = awaitItem()
|
||||||
assertThat(loadingState.sendActionState).isEqualTo(Async.Loading<Unit>())
|
assertThat(loadingState.sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(failureState.sendActionState).isEqualTo(Async.Failure<Unit>(failure))
|
assertThat(failureState.sendActionState).isEqualTo((SendActionState.Failure(failure)))
|
||||||
assertThat(room.sendMediaCount).isEqualTo(0)
|
assertThat(room.sendMediaCount).isEqualTo(0)
|
||||||
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
||||||
val clearedState = awaitItem()
|
val clearedState = awaitItem()
|
||||||
assertThat(clearedState.sendActionState).isEqualTo(Async.Uninitialized)
|
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,13 @@ class MessageComposerPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - Pick file from storage`() = runTest {
|
fun `present - Pick file from storage`() = runTest {
|
||||||
val room = FakeMatrixRoom()
|
val room = FakeMatrixRoom()
|
||||||
|
room.givenProgressCallbackValues(
|
||||||
|
listOf(
|
||||||
|
Pair(0, 10),
|
||||||
|
Pair(5, 10),
|
||||||
|
Pair(10, 10)
|
||||||
|
)
|
||||||
|
)
|
||||||
val presenter = createPresenter(this, room = room)
|
val presenter = createPresenter(this, room = room)
|
||||||
moleculeFlow(RecompositionClock.Immediate) {
|
moleculeFlow(RecompositionClock.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -376,7 +383,10 @@ class MessageComposerPresenterTest {
|
||||||
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
initialState.eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles)
|
||||||
val sendingState = awaitItem()
|
val sendingState = awaitItem()
|
||||||
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
assertThat(sendingState.showAttachmentSourcePicker).isFalse()
|
||||||
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending::class.java)
|
assertThat(sendingState.attachmentsState).isInstanceOf(AttachmentsState.Sending.Processing::class.java)
|
||||||
|
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(0f))
|
||||||
|
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(0.5f))
|
||||||
|
assertThat(awaitItem().attachmentsState).isEqualTo(AttachmentsState.Sending.Uploading(1f))
|
||||||
val sentState = awaitItem()
|
val sentState = awaitItem()
|
||||||
assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None)
|
assertThat(sentState.attachmentsState).isEqualTo(AttachmentsState.None)
|
||||||
assertThat(room.sendMediaCount).isEqualTo(1)
|
assertThat(room.sendMediaCount).isEqualTo(1)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -41,6 +42,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
fun ProgressDialog(
|
fun ProgressDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
|
type: ProgressDialogType = ProgressDialogType.Indeterminate,
|
||||||
onDismiss: () -> Unit = {},
|
onDismiss: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Dialog(
|
Dialog(
|
||||||
|
|
@ -50,14 +52,40 @@ fun ProgressDialog(
|
||||||
ProgressDialogContent(
|
ProgressDialogContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = text,
|
text = text,
|
||||||
|
progressIndicator = {
|
||||||
|
when (type) {
|
||||||
|
is ProgressDialogType.Indeterminate -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ProgressDialogType.Determinate -> {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = type.progress,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface ProgressDialogType {
|
||||||
|
data class Determinate(val progress: Float) : ProgressDialogType
|
||||||
|
object Indeterminate : ProgressDialogType
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ProgressDialogContent(
|
private fun ProgressDialogContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
|
progressIndicator: @Composable () -> Unit = {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
@ -71,9 +99,7 @@ private fun ProgressDialogContent(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp)
|
modifier = Modifier.padding(top = 38.dp, bottom = 32.dp, start = 40.dp, end = 40.dp)
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator(
|
progressIndicator()
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
if (!text.isNullOrBlank()) {
|
if (!text.isNullOrBlank()) {
|
||||||
Spacer(modifier = Modifier.height(22.dp))
|
Spacer(modifier = Modifier.height(22.dp))
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -78,6 +79,7 @@ class FakeMatrixRoom(
|
||||||
private var forwardEventResult = Result.success(Unit)
|
private var forwardEventResult = Result.success(Unit)
|
||||||
private var reportContentResult = Result.success(Unit)
|
private var reportContentResult = Result.success(Unit)
|
||||||
private var sendLocationResult = Result.success(Unit)
|
private var sendLocationResult = Result.success(Unit)
|
||||||
|
private var progressCallbackValues = emptyList<Pair<Long, Long>>()
|
||||||
|
|
||||||
var sendMediaCount = 0
|
var sendMediaCount = 0
|
||||||
private set
|
private set
|
||||||
|
|
@ -152,7 +154,7 @@ class FakeMatrixRoom(
|
||||||
return toggleReactionResult
|
return toggleReactionResult
|
||||||
}
|
}
|
||||||
|
|
||||||
if(_myReactions.contains(emoji)) {
|
if (_myReactions.contains(emoji)) {
|
||||||
_myReactions.remove(emoji)
|
_myReactions.remove(emoji)
|
||||||
} else {
|
} else {
|
||||||
_myReactions.add(emoji)
|
_myReactions.add(emoji)
|
||||||
|
|
@ -229,20 +231,26 @@ class FakeMatrixRoom(
|
||||||
thumbnailFile: File,
|
thumbnailFile: File,
|
||||||
imageInfo: ImageInfo,
|
imageInfo: ImageInfo,
|
||||||
progressCallback: ProgressCallback?
|
progressCallback: ProgressCallback?
|
||||||
): Result<Unit> = fakeSendMedia()
|
): Result<Unit> = fakeSendMedia(progressCallback)
|
||||||
|
|
||||||
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
|
override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia(
|
||||||
|
progressCallback
|
||||||
|
)
|
||||||
|
|
||||||
override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
|
override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia(progressCallback)
|
||||||
|
|
||||||
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia()
|
override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result<Unit> = fakeSendMedia(progressCallback)
|
||||||
|
|
||||||
override suspend fun forwardEvent(eventId: EventId, rooms: List<RoomId>): Result<Unit> = simulateLongTask {
|
override suspend fun forwardEvent(eventId: EventId, rooms: List<RoomId>): Result<Unit> = simulateLongTask {
|
||||||
forwardEventResult
|
forwardEventResult
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fakeSendMedia(): Result<Unit> = simulateLongTask {
|
private suspend fun fakeSendMedia(progressCallback: ProgressCallback?): Result<Unit> = simulateLongTask {
|
||||||
sendMediaResult.onSuccess {
|
sendMediaResult.onSuccess {
|
||||||
|
progressCallbackValues.forEach { (current, total) ->
|
||||||
|
progressCallback?.onProgress(current, total)
|
||||||
|
delay(1)
|
||||||
|
}
|
||||||
sendMediaCount++
|
sendMediaCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -381,4 +389,8 @@ class FakeMatrixRoom(
|
||||||
fun givenSendLocationResult(result: Result<Unit>) {
|
fun givenSendLocationResult(result: Result<Unit>) {
|
||||||
sendLocationResult = result
|
sendLocationResult = result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenProgressCallbackValues(values: List<Pair<Long, Long>>) {
|
||||||
|
progressCallbackValues = values
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ package io.element.android.libraries.mediaupload.api
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import io.element.android.libraries.core.extensions.flatMap
|
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 io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -26,7 +27,12 @@ class MediaSender @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun sendMedia(uri: Uri, mimeType: String, compressIfPossible: Boolean): Result<Unit> {
|
suspend fun sendMedia(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
compressIfPossible: Boolean,
|
||||||
|
progressCallback: ProgressCallback? = null
|
||||||
|
): Result<Unit> {
|
||||||
return preProcessor
|
return preProcessor
|
||||||
.process(
|
.process(
|
||||||
uri = uri,
|
uri = uri,
|
||||||
|
|
@ -35,12 +41,13 @@ class MediaSender @Inject constructor(
|
||||||
compressIfPossible = compressIfPossible
|
compressIfPossible = compressIfPossible
|
||||||
)
|
)
|
||||||
.flatMap { info ->
|
.flatMap { info ->
|
||||||
room.sendMedia(info)
|
room.sendMedia(info, progressCallback)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun MatrixRoom.sendMedia(
|
private suspend fun MatrixRoom.sendMedia(
|
||||||
info: MediaUploadInfo,
|
info: MediaUploadInfo,
|
||||||
|
progressCallback: ProgressCallback?
|
||||||
): Result<Unit> {
|
): Result<Unit> {
|
||||||
return when (info) {
|
return when (info) {
|
||||||
is MediaUploadInfo.Image -> {
|
is MediaUploadInfo.Image -> {
|
||||||
|
|
@ -48,7 +55,7 @@ class MediaSender @Inject constructor(
|
||||||
file = info.file,
|
file = info.file,
|
||||||
thumbnailFile = info.thumbnailFile,
|
thumbnailFile = info.thumbnailFile,
|
||||||
imageInfo = info.info,
|
imageInfo = info.info,
|
||||||
progressCallback = null
|
progressCallback = progressCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +64,7 @@ class MediaSender @Inject constructor(
|
||||||
file = info.file,
|
file = info.file,
|
||||||
thumbnailFile = info.thumbnailFile,
|
thumbnailFile = info.thumbnailFile,
|
||||||
videoInfo = info.info,
|
videoInfo = info.info,
|
||||||
progressCallback = null
|
progressCallback = progressCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,7 +72,7 @@ class MediaSender @Inject constructor(
|
||||||
sendFile(
|
sendFile(
|
||||||
file = info.file,
|
file = info.file,
|
||||||
fileInfo = info.info,
|
fileInfo = info.info,
|
||||||
progressCallback = null
|
progressCallback = progressCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info"))
|
else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info"))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:99720bb9fe43eed7901c4e093da7a82000ea6721041095486719283fdec2eb52
|
oid sha256:3a968039c2fa94e89c75e395bab808d2960c37c43576b558438ec17bcd17e507
|
||||||
size 185514
|
size 184797
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue