Close the media preview screen ASAP with sending queue enabled (#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 <android@element.io>
This commit is contained in:
parent
28cf775c32
commit
f72fb9650b
15 changed files with 286 additions and 126 deletions
|
|
@ -12,6 +12,6 @@ import androidx.compose.runtime.Immutable
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface AttachmentsPreviewEvents {
|
sealed interface AttachmentsPreviewEvents {
|
||||||
data object SendAttachment : AttachmentsPreviewEvents
|
data object SendAttachment : AttachmentsPreviewEvents
|
||||||
data object Cancel : AttachmentsPreviewEvents
|
data object CancelAndDismiss : AttachmentsPreviewEvents
|
||||||
data object ClearSendState : AttachmentsPreviewEvents
|
data object CancelAndClearSendState : AttachmentsPreviewEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,18 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import dagger.assisted.Assisted
|
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.androidutils.file.TemporaryUriDeleter
|
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||||
import io.element.android.libraries.androidutils.file.safeDelete
|
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.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.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.core.ProgressCallback
|
||||||
|
|
@ -48,6 +52,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
private val permalinkBuilder: PermalinkBuilder,
|
private val permalinkBuilder: PermalinkBuilder,
|
||||||
private val temporaryUriDeleter: TemporaryUriDeleter,
|
private val temporaryUriDeleter: TemporaryUriDeleter,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
|
||||||
|
private val dispatchers: CoroutineDispatchers,
|
||||||
) : Presenter<AttachmentsPreviewState> {
|
) : Presenter<AttachmentsPreviewState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -72,71 +78,79 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }
|
||||||
|
|
||||||
val userSentAttachment = remember { mutableStateOf(false) }
|
|
||||||
val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false)
|
val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false)
|
||||||
val showCaptionCompatibilityWarning by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning).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) {
|
LaunchedEffect(Unit) {
|
||||||
preProcessAttachment(
|
useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
||||||
|
|
||||||
|
preprocessMediaJob = preProcessAttachment(
|
||||||
attachment,
|
attachment,
|
||||||
mediaUploadInfoState,
|
sendActionState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(userSentAttachment.value, mediaUploadInfoState.value) {
|
val observableSendState = snapshotFlow { sendActionState.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) {
|
fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
|
||||||
when (attachmentsPreviewEvents) {
|
when (attachmentsPreviewEvents) {
|
||||||
is AttachmentsPreviewEvents.SendAttachment -> coroutineScope.launch {
|
is AttachmentsPreviewEvents.SendAttachment -> {
|
||||||
val useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
|
ongoingSendAttachmentJob.value = coroutineScope.launch {
|
||||||
userSentAttachment.value = true
|
// If the processing was hidden before, make it visible now
|
||||||
val instantSending = mediaUploadInfoState.value.isReady() && useSendQueue
|
if (sendActionState.value is SendActionState.Sending.Processing) {
|
||||||
sendActionState.value = if (instantSending) {
|
sendActionState.value = SendActionState.Sending.Processing(displayProgress = true)
|
||||||
SendActionState.Sending.InstantSending
|
}
|
||||||
} else {
|
|
||||||
SendActionState.Sending.Processing
|
// 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() }
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
caption = caption,
|
||||||
|
sendActionState = sendActionState,
|
||||||
|
dismissAfterSend = !useSendQueue,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.Cancel -> {
|
AttachmentsPreviewEvents.CancelAndDismiss -> {
|
||||||
coroutineScope.cancel(
|
// Cancel media preprocessing and sending
|
||||||
|
preprocessMediaJob?.cancel()
|
||||||
|
ongoingSendAttachmentJob.value?.cancel()
|
||||||
|
|
||||||
|
// Dismiss the screen
|
||||||
|
dismiss(
|
||||||
attachment,
|
attachment,
|
||||||
mediaUploadInfoState.value,
|
|
||||||
sendActionState,
|
sendActionState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AttachmentsPreviewEvents.ClearSendState -> {
|
AttachmentsPreviewEvents.CancelAndClearSendState -> {
|
||||||
|
// Cancel media sending
|
||||||
ongoingSendAttachmentJob.value?.let {
|
ongoingSendAttachmentJob.value?.let {
|
||||||
it.cancel()
|
it.cancel()
|
||||||
ongoingSendAttachmentJob.value = null
|
ongoingSendAttachmentJob.value = null
|
||||||
}
|
}
|
||||||
sendActionState.value = SendActionState.Idle
|
|
||||||
|
val mediaUploadInfo = sendActionState.value.mediaUploadInfo()
|
||||||
|
sendActionState.value = if (mediaUploadInfo != null) {
|
||||||
|
SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
|
||||||
|
} else {
|
||||||
|
SendActionState.Idle
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,13 +167,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private fun CoroutineScope.preProcessAttachment(
|
private fun CoroutineScope.preProcessAttachment(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) = launch(dispatchers.io) {
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
preProcessMedia(
|
preProcessMedia(
|
||||||
mediaAttachment = attachment,
|
mediaAttachment = attachment,
|
||||||
mediaUploadInfoState = mediaUploadInfoState,
|
sendActionState = sendActionState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,37 +181,36 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
|
|
||||||
private suspend fun preProcessMedia(
|
private suspend fun preProcessMedia(
|
||||||
mediaAttachment: Attachment.Media,
|
mediaAttachment: Attachment.Media,
|
||||||
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) {
|
) {
|
||||||
mediaUploadInfoState.value = AsyncData.Loading()
|
sendActionState.value = SendActionState.Sending.Processing(displayProgress = false)
|
||||||
mediaSender.preProcessMedia(
|
mediaSender.preProcessMedia(
|
||||||
uri = mediaAttachment.localMedia.uri,
|
uri = mediaAttachment.localMedia.uri,
|
||||||
mimeType = mediaAttachment.localMedia.info.mimeType,
|
mimeType = mediaAttachment.localMedia.info.mimeType,
|
||||||
).fold(
|
).fold(
|
||||||
onSuccess = { mediaUploadInfo ->
|
onSuccess = { mediaUploadInfo ->
|
||||||
mediaUploadInfoState.value = AsyncData.Success(mediaUploadInfo)
|
sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
|
||||||
},
|
},
|
||||||
onFailure = {
|
onFailure = {
|
||||||
Timber.e(it, "Failed to pre-process media")
|
Timber.e(it, "Failed to pre-process media")
|
||||||
if (it is CancellationException) {
|
if (it is CancellationException) {
|
||||||
throw it
|
throw it
|
||||||
} else {
|
} else {
|
||||||
mediaUploadInfoState.value = AsyncData.Failure(it)
|
sendActionState.value = SendActionState.Failure(it, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.cancel(
|
private fun dismiss(
|
||||||
attachment: Attachment,
|
attachment: Attachment,
|
||||||
mediaUploadInfo: AsyncData<MediaUploadInfo>,
|
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
) = launch {
|
) {
|
||||||
// Delete the temporary file
|
// Delete the temporary file
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
temporaryUriDeleter.delete(attachment.localMedia.uri)
|
||||||
mediaUploadInfo.dataOrNull()?.let { data ->
|
sendActionState.value.mediaUploadInfo()?.let { data ->
|
||||||
cleanUp(data)
|
cleanUp(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -219,13 +232,14 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
mediaUploadInfo: MediaUploadInfo,
|
mediaUploadInfo: MediaUploadInfo,
|
||||||
caption: String?,
|
caption: String?,
|
||||||
sendActionState: MutableState<SendActionState>,
|
sendActionState: MutableState<SendActionState>,
|
||||||
|
dismissAfterSend: Boolean,
|
||||||
) = runCatching {
|
) = runCatching {
|
||||||
val context = coroutineContext
|
val context = coroutineContext
|
||||||
val progressCallback = object : ProgressCallback {
|
val progressCallback = object : ProgressCallback {
|
||||||
override fun onProgress(current: Long, total: Long) {
|
override fun onProgress(current: Long, total: Long) {
|
||||||
// Note will not happen if useSendQueue is true
|
// Note will not happen if useSendQueue is true
|
||||||
if (context.isActive) {
|
if (context.isActive) {
|
||||||
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
|
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat(), mediaUploadInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,14 +254,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
cleanUp(mediaUploadInfo)
|
cleanUp(mediaUploadInfo)
|
||||||
// Reset the sendActionState to ensure that dialog is closed before the screen
|
// Reset the sendActionState to ensure that dialog is closed before the screen
|
||||||
sendActionState.value = SendActionState.Done
|
sendActionState.value = SendActionState.Done
|
||||||
onDoneListener()
|
|
||||||
|
if (dismissAfterSend) {
|
||||||
|
onDoneListener()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onFailure = { error ->
|
onFailure = { error ->
|
||||||
Timber.e(error, "Failed to send attachment")
|
Timber.e(error, "Failed to send attachment")
|
||||||
if (error is CancellationException) {
|
if (error is CancellationException) {
|
||||||
throw error
|
throw error
|
||||||
} else {
|
} else {
|
||||||
sendActionState.value = SendActionState.Failure(error)
|
sendActionState.value = SendActionState.Failure(error, mediaUploadInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
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
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
|
|
||||||
data class AttachmentsPreviewState(
|
data class AttachmentsPreviewState(
|
||||||
|
|
@ -26,11 +27,18 @@ sealed interface SendActionState {
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
sealed interface Sending : SendActionState {
|
sealed interface Sending : SendActionState {
|
||||||
data object InstantSending : Sending
|
data class Processing(val displayProgress: Boolean) : Sending
|
||||||
data object Processing : Sending
|
data class ReadyToUpload(val mediaInfo: MediaUploadInfo) : Sending
|
||||||
data class Uploading(val progress: Float) : 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
|
data object Done : SendActionState
|
||||||
|
|
||||||
|
fun mediaUploadInfo(): MediaUploadInfo? = when (this) {
|
||||||
|
is Sending.ReadyToUpload -> mediaInfo
|
||||||
|
is Sending.Uploading -> mediaUploadInfo
|
||||||
|
is Failure -> mediaUploadInfo
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,25 @@ package io.element.android.features.messages.impl.attachments.preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
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.MediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.textcomposer.model.TextEditorState
|
import io.element.android.libraries.textcomposer.model.TextEditorState
|
||||||
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
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(sendActionState = SendActionState.Sending.Processing),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = false)),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
|
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = true)),
|
||||||
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
|
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(allowCaption = false),
|
||||||
anAttachmentsPreviewState(showCaptionCompatibilityWarning = true),
|
anAttachmentsPreviewState(showCaptionCompatibilityWarning = true),
|
||||||
)
|
)
|
||||||
|
|
@ -44,3 +50,20 @@ fun anAttachmentsPreviewState(
|
||||||
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
|
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
|
||||||
eventSink = {}
|
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) },
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,11 @@ fun AttachmentsPreviewView(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postCancel() {
|
fun postCancel() {
|
||||||
state.eventSink(AttachmentsPreviewEvents.Cancel)
|
state.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postClearSendState() {
|
fun postClearSendState() {
|
||||||
state.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
state.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = state.sendActionState !is SendActionState.Sending) {
|
BackHandler(enabled = state.sendActionState !is SendActionState.Sending) {
|
||||||
|
|
@ -106,12 +106,14 @@ private fun AttachmentSendStateView(
|
||||||
) {
|
) {
|
||||||
when (sendActionState) {
|
when (sendActionState) {
|
||||||
is SendActionState.Sending.Processing -> {
|
is SendActionState.Sending.Processing -> {
|
||||||
ProgressDialog(
|
if (sendActionState.displayProgress) {
|
||||||
type = ProgressDialogType.Indeterminate,
|
ProgressDialog(
|
||||||
text = stringResource(id = CommonStrings.common_sending),
|
type = ProgressDialogType.Indeterminate,
|
||||||
showCancelButton = true,
|
text = stringResource(id = CommonStrings.common_sending),
|
||||||
onDismissRequest = onDismissClick,
|
showCancelButton = true,
|
||||||
)
|
onDismissRequest = onDismissClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is SendActionState.Sending.Uploading -> {
|
is SendActionState.Sending.Uploading -> {
|
||||||
ProgressDialog(
|
ProgressDialog(
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import io.element.android.features.messages.impl.attachments.preview.OnDoneListe
|
||||||
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
import io.element.android.features.messages.impl.attachments.preview.SendActionState
|
||||||
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
|
||||||
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
|
||||||
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
import io.element.android.libraries.matrix.api.core.ProgressCallback
|
||||||
|
|
@ -35,20 +36,24 @@ import io.element.android.libraries.matrix.test.permalink.FakePermalinkBuilder
|
||||||
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
|
||||||
import io.element.android.libraries.mediaupload.api.MediaSender
|
import io.element.android.libraries.mediaupload.api.MediaSender
|
||||||
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
|
||||||
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.awaitLastSequentialItem
|
||||||
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
import io.element.android.tests.testutils.fake.FakeTemporaryUriDeleter
|
||||||
import io.element.android.tests.testutils.lambda.any
|
import io.element.android.tests.testutils.lambda.any
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
import kotlinx.coroutines.test.advanceUntilIdle
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -121,13 +126,14 @@ class AttachmentsPreviewPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f))
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0f, mediaUploadInfo))
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(0.5f, mediaUploadInfo))
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Uploading(1f, mediaUploadInfo))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
|
@ -159,10 +165,10 @@ class AttachmentsPreviewPresenterTest {
|
||||||
// Pre-processing finishes
|
// Pre-processing finishes
|
||||||
processLatch.complete(Unit)
|
processLatch.complete(Unit)
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.InstantSending)
|
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
|
@ -191,12 +197,13 @@ class AttachmentsPreviewPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
// Pre-processing finishes
|
// Pre-processing finishes
|
||||||
processLatch.complete(Unit)
|
processLatch.complete(Unit)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = true))
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
onDoneListener.assertions().isCalledOnce()
|
onDoneListener.assertions().isCalledOnce()
|
||||||
|
|
@ -222,8 +229,7 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
|
||||||
// Pre-processing finishes
|
// Pre-processing finishes
|
||||||
processLatch.complete(Unit)
|
processLatch.complete(Unit)
|
||||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||||
|
|
@ -252,8 +258,7 @@ class AttachmentsPreviewPresenterTest {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.InstantSending)
|
|
||||||
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Failure::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,7 +276,7 @@ class AttachmentsPreviewPresenterTest {
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.Cancel)
|
initialState.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
deleteCallback.assertions().isCalledOnce()
|
deleteCallback.assertions().isCalledOnce()
|
||||||
|
|
@ -305,8 +310,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendImageResult.assertions().isCalledOnce().with(
|
sendImageResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -346,8 +351,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendVideoResult.assertions().isCalledOnce().with(
|
sendVideoResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -385,8 +390,8 @@ class AttachmentsPreviewPresenterTest {
|
||||||
initialState.textEditorState.setMarkdown(A_CAPTION)
|
initialState.textEditorState.setMarkdown(A_CAPTION)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isInstanceOf(SendActionState.Sending.ReadyToUpload::class.java)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Done)
|
||||||
sendAudioResult.assertions().isCalledOnce().with(
|
sendAudioResult.assertions().isCalledOnce().with(
|
||||||
any(),
|
any(),
|
||||||
|
|
@ -400,7 +405,7 @@ class AttachmentsPreviewPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - send media failure scenario`() = runTest {
|
fun `present - send media failure scenario without media queue`() = runTest {
|
||||||
val failure = MediaPreProcessor.Failure(null)
|
val failure = MediaPreProcessor.Failure(null)
|
||||||
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
Result.failure(failure)
|
Result.failure(failure)
|
||||||
|
|
@ -408,7 +413,7 @@ class AttachmentsPreviewPresenterTest {
|
||||||
val room = FakeMatrixRoom(
|
val room = FakeMatrixRoom(
|
||||||
sendFileResult = sendFileResult,
|
sendFileResult = sendFileResult,
|
||||||
)
|
)
|
||||||
val presenter = createAttachmentsPreviewPresenter(room = room)
|
val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = false)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -416,20 +421,28 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure))
|
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure, mediaUploadInfo))
|
||||||
sendFileResult.assertions().isCalledOnce()
|
sendFileResult.assertions().isCalledOnce()
|
||||||
failureState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
failureState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||||
val clearedState = awaitItem()
|
val clearedState = awaitLastSequentialItem()
|
||||||
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - dismissing the progress dialog stops media upload`() = runTest {
|
fun `present - send media failure scenario with media queue`() = runTest {
|
||||||
val presenter = createAttachmentsPreviewPresenter()
|
val failure = MediaPreProcessor.Failure(null)
|
||||||
|
val sendFileResult = lambdaRecorder<File, FileInfo, String?, String?, ProgressCallback?, Result<FakeMediaUploadHandler>> { _, _, _, _, _ ->
|
||||||
|
Result.failure(failure)
|
||||||
|
}
|
||||||
|
val onDoneListenerResult = lambdaRecorder<Unit> {}
|
||||||
|
val room = FakeMatrixRoom(
|
||||||
|
sendFileResult = sendFileResult,
|
||||||
|
)
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(room = room, mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
|
@ -437,14 +450,62 @@ class AttachmentsPreviewPresenterTest {
|
||||||
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing)
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
initialState.eventSink(AttachmentsPreviewEvents.ClearSendState)
|
|
||||||
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
// Check that the onDoneListener is called so the screen would be dismissed
|
||||||
|
onDoneListenerResult.assertions().isCalledOnce()
|
||||||
|
|
||||||
|
val failureState = awaitItem()
|
||||||
|
assertThat(failureState.sendActionState).isEqualTo(SendActionState.Failure(failure, mediaUploadInfo))
|
||||||
|
sendFileResult.assertions().isCalledOnce()
|
||||||
|
failureState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||||
|
val clearedState = awaitLastSequentialItem()
|
||||||
|
assertThat(clearedState.sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createAttachmentsPreviewPresenter(
|
@Test
|
||||||
|
fun `present - dismissing the progress dialog stops media upload without media queue`() = runTest {
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(mediaUploadOnSendQueueEnabled = false)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||||
|
// The sending is cancelled and the state is kept at ReadyToUpload
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - dismissing the progress dialog stops media upload with media queue`() = runTest {
|
||||||
|
val onDoneListenerResult = lambdaRecorder<Unit> {}
|
||||||
|
val presenter = createAttachmentsPreviewPresenter(mediaUploadOnSendQueueEnabled = true, onDoneListener = onDoneListenerResult)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.SendAttachment)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Idle)
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.Processing(displayProgress = false))
|
||||||
|
assertThat(awaitItem().sendActionState).isEqualTo(SendActionState.Sending.ReadyToUpload(mediaUploadInfo))
|
||||||
|
initialState.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
|
||||||
|
// The sending is cancelled and the state is kept at ReadyToUpload
|
||||||
|
ensureAllEventsConsumed()
|
||||||
|
|
||||||
|
// Check that the onDoneListener is called so the screen would be dismissed
|
||||||
|
onDoneListenerResult.assertions().isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun TestScope.createAttachmentsPreviewPresenter(
|
||||||
localMedia: LocalMedia = aLocalMedia(
|
localMedia: LocalMedia = aLocalMedia(
|
||||||
uri = mockMediaUrl,
|
uri = mockMediaUrl,
|
||||||
),
|
),
|
||||||
|
|
@ -469,7 +530,19 @@ class AttachmentsPreviewPresenterTest {
|
||||||
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
FeatureFlags.MediaCaptionCreation.key to allowCaption,
|
||||||
FeatureFlags.MediaCaptionWarning.key to showCaptionCompatibilityWarning,
|
FeatureFlags.MediaCaptionWarning.key to showCaptionCompatibilityWarning,
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
|
sessionCoroutineScope = this,
|
||||||
|
dispatchers = testCoroutineDispatchers(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val mediaUploadInfo = MediaUploadInfo.AnyFile(
|
||||||
|
File("test"),
|
||||||
|
FileInfo(
|
||||||
|
mimetype = MimeTypes.Any,
|
||||||
|
size = 999L,
|
||||||
|
thumbnailInfo = null,
|
||||||
|
thumbnailSource = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.core.coroutine
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first element of the flow that is an instance of [T], waiting for it if necessary.
|
||||||
|
*/
|
||||||
|
suspend inline fun <reified T> Flow<*>.firstInstanceOf(): T {
|
||||||
|
return first { it is T } as T
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
|
import android.media.MediaMetadataRetriever.OPTION_CLOSEST_SYNC
|
||||||
import android.media.ThumbnailUtils
|
import android.media.ThumbnailUtils
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
|
|
@ -18,6 +19,7 @@ import android.provider.MediaStore
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.vanniktech.blurhash.BlurHash
|
import com.vanniktech.blurhash.BlurHash
|
||||||
|
import io.element.android.libraries.androidutils.bitmap.resizeToMax
|
||||||
import io.element.android.libraries.androidutils.file.createTmpFile
|
import io.element.android.libraries.androidutils.file.createTmpFile
|
||||||
import io.element.android.libraries.androidutils.media.runAndRelease
|
import io.element.android.libraries.androidutils.media.runAndRelease
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
|
@ -89,7 +91,11 @@ class ThumbnailFactory @Inject constructor(
|
||||||
return createThumbnail(mimeType = MimeTypes.Jpeg) {
|
return createThumbnail(mimeType = MimeTypes.Jpeg) {
|
||||||
MediaMetadataRetriever().runAndRelease {
|
MediaMetadataRetriever().runAndRelease {
|
||||||
setDataSource(context, file.toUri())
|
setDataSource(context, file.toUri())
|
||||||
getFrameAtTime(VIDEO_THUMB_FRAME)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
getScaledFrameAtTime(VIDEO_THUMB_FRAME, OPTION_CLOSEST_SYNC, THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT)
|
||||||
|
} else {
|
||||||
|
getFrameAtTime(VIDEO_THUMB_FRAME)?.resizeToMax(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:26764da6595dee37219d5bbf3e53bad8db6451d7d39fa7741c61d96c89938bf8
|
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||||
size 51246
|
size 395102
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:b9d5ee3eb77075ca147fc7ed96c83a846dcfcf8e09bc129a24daa5ed19f262a7
|
oid sha256:26764da6595dee37219d5bbf3e53bad8db6451d7d39fa7741c61d96c89938bf8
|
||||||
size 51217
|
size 51246
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9f2882937b7dd90da5d1708185f0130b75659eed57eaf2a966280a891f4ddbd6
|
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||||
size 89083
|
size 395102
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7af7dedec8de4ac367fc870d2c235060b3a57d27b2bb879dc784a3cef3c2a370
|
oid sha256:b9d5ee3eb77075ca147fc7ed96c83a846dcfcf8e09bc129a24daa5ed19f262a7
|
||||||
size 390628
|
size 51217
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
oid sha256:9f2882937b7dd90da5d1708185f0130b75659eed57eaf2a966280a891f4ddbd6
|
||||||
size 395102
|
size 89083
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7af7dedec8de4ac367fc870d2c235060b3a57d27b2bb879dc784a3cef3c2a370
|
||||||
|
size 390628
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:603ff14c11328797ad3ff07002aa77397d1e9f8c39291f9c762776b591d68ada
|
||||||
|
size 395102
|
||||||
Loading…
Add table
Add a link
Reference in a new issue