Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
daedric7 committed Jan 8, 2025
2 parents d5dadf1 + 65ce91a commit 3e0f87b
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 120 deletions.
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ licensee {
allowUrl("https://asm.ow2.io/license.html")
allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt")
ignoreDependencies("com.github.matrix-org", "matrix-analytics-events")
// Ignore dependency that are not third-party licenses to us.
ignoreDependencies(groupId = "io.element.android")
}

fun Project.configureLicensesTasks(reportingExtension: ReportingExtension) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ import androidx.compose.runtime.Immutable
@Immutable
sealed interface AttachmentsPreviewEvents {
data object SendAttachment : AttachmentsPreviewEvents
data object Cancel : AttachmentsPreviewEvents
data object ClearSendState : AttachmentsPreviewEvents
data object CancelAndDismiss : AttachmentsPreviewEvents
data object CancelAndClearSendState : AttachmentsPreviewEvents
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.firstInstanceOf
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.core.ProgressCallback
Expand All @@ -48,6 +52,8 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
private val permalinkBuilder: PermalinkBuilder,
private val temporaryUriDeleter: TemporaryUriDeleter,
private val featureFlagService: FeatureFlagService,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
) : Presenter<AttachmentsPreviewState> {
@AssistedFactory
interface Factory {
Expand All @@ -72,71 +78,79 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(

val ongoingSendAttachmentJob = remember { mutableStateOf<Job?>(null) }

val userSentAttachment = remember { mutableStateOf(false) }
val allowCaption by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionCreation).collectAsState(initial = false)
val showCaptionCompatibilityWarning by featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaCaptionWarning).collectAsState(initial = false)

val mediaUploadInfoState = remember { mutableStateOf<AsyncData<MediaUploadInfo>>(AsyncData.Uninitialized) }
var useSendQueue by remember { mutableStateOf(false) }
var preprocessMediaJob by remember { mutableStateOf<Job?>(null) }
LaunchedEffect(Unit) {
preProcessAttachment(
useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)

preprocessMediaJob = preProcessAttachment(
attachment,
mediaUploadInfoState,
sendActionState
)
}

LaunchedEffect(userSentAttachment.value, mediaUploadInfoState.value) {
if (userSentAttachment.value) {
// User confirmed sending the attachment
when (val mediaUploadInfo = mediaUploadInfoState.value) {
is AsyncData.Success -> {
val observableSendState = snapshotFlow { sendActionState.value }

fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
when (attachmentsPreviewEvents) {
is AttachmentsPreviewEvents.SendAttachment -> {
ongoingSendAttachmentJob.value = coroutineScope.launch {
// If the processing was hidden before, make it visible now
if (sendActionState.value is SendActionState.Sending.Processing) {
sendActionState.value = SendActionState.Sending.Processing(displayProgress = true)
}

// Wait until the media is ready to be uploaded
val mediaUploadInfo = observableSendState.firstInstanceOf<SendActionState.Sending.ReadyToUpload>().mediaInfo

// Pre-processing is done, send the attachment
val caption = markdownTextEditorState.getMessageMarkdown(permalinkBuilder)
.takeIf { it.isNotEmpty() }
ongoingSendAttachmentJob.value = coroutineScope.launch {

// If we're supposed to send the media as a background job, we can dismiss this screen already
if (useSendQueue && coroutineContext.isActive) {
onDoneListener()
}

// If using the send queue, send it using the session coroutine scope so it doesn't matter if this screen or the chat one are closed
val sendMediaCoroutineScope = if (useSendQueue) sessionCoroutineScope else coroutineScope
sendMediaCoroutineScope.launch(dispatchers.io) {
sendPreProcessedMedia(
mediaUploadInfo = mediaUploadInfo.data,
mediaUploadInfo = mediaUploadInfo,
caption = caption,
sendActionState = sendActionState,
dismissAfterSend = !useSendQueue,
)
}
}
is AsyncData.Failure -> {
// Pre-processing has failed, show the error
sendActionState.value = SendActionState.Failure(mediaUploadInfo.error)
}
AsyncData.Uninitialized,
is AsyncData.Loading -> {
// Pre-processing is still in progress, do nothing
}
}
}
}
AttachmentsPreviewEvents.CancelAndDismiss -> {
// Cancel media preprocessing and sending
preprocessMediaJob?.cancel()
ongoingSendAttachmentJob.value?.cancel()

fun handleEvents(attachmentsPreviewEvents: AttachmentsPreviewEvents) {
when (attachmentsPreviewEvents) {
is AttachmentsPreviewEvents.SendAttachment -> coroutineScope.launch {
val useSendQueue = featureFlagService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
userSentAttachment.value = true
val instantSending = mediaUploadInfoState.value.isReady() && useSendQueue
sendActionState.value = if (instantSending) {
SendActionState.Sending.InstantSending
} else {
SendActionState.Sending.Processing
}
}
AttachmentsPreviewEvents.Cancel -> {
coroutineScope.cancel(
// Dismiss the screen
dismiss(
attachment,
mediaUploadInfoState.value,
sendActionState,
)
}
AttachmentsPreviewEvents.ClearSendState -> {
AttachmentsPreviewEvents.CancelAndClearSendState -> {
// Cancel media sending
ongoingSendAttachmentJob.value?.let {
it.cancel()
ongoingSendAttachmentJob.value = null
}
sendActionState.value = SendActionState.Idle

val mediaUploadInfo = sendActionState.value.mediaUploadInfo()
sendActionState.value = if (mediaUploadInfo != null) {
SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
} else {
SendActionState.Idle
}
}
}
}
Expand All @@ -153,51 +167,50 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(

private fun CoroutineScope.preProcessAttachment(
attachment: Attachment,
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
) = launch {
sendActionState: MutableState<SendActionState>,
) = launch(dispatchers.io) {
when (attachment) {
is Attachment.Media -> {
preProcessMedia(
mediaAttachment = attachment,
mediaUploadInfoState = mediaUploadInfoState,
sendActionState = sendActionState,
)
}
}
}

private suspend fun preProcessMedia(
mediaAttachment: Attachment.Media,
mediaUploadInfoState: MutableState<AsyncData<MediaUploadInfo>>,
sendActionState: MutableState<SendActionState>,
) {
mediaUploadInfoState.value = AsyncData.Loading()
sendActionState.value = SendActionState.Sending.Processing(displayProgress = false)
mediaSender.preProcessMedia(
uri = mediaAttachment.localMedia.uri,
mimeType = mediaAttachment.localMedia.info.mimeType,
).fold(
onSuccess = { mediaUploadInfo ->
mediaUploadInfoState.value = AsyncData.Success(mediaUploadInfo)
sendActionState.value = SendActionState.Sending.ReadyToUpload(mediaUploadInfo)
},
onFailure = {
Timber.e(it, "Failed to pre-process media")
if (it is CancellationException) {
throw it
} else {
mediaUploadInfoState.value = AsyncData.Failure(it)
sendActionState.value = SendActionState.Failure(it, null)
}
}
)
}

private fun CoroutineScope.cancel(
private fun dismiss(
attachment: Attachment,
mediaUploadInfo: AsyncData<MediaUploadInfo>,
sendActionState: MutableState<SendActionState>,
) = launch {
) {
// Delete the temporary file
when (attachment) {
is Attachment.Media -> {
temporaryUriDeleter.delete(attachment.localMedia.uri)
mediaUploadInfo.dataOrNull()?.let { data ->
sendActionState.value.mediaUploadInfo()?.let { data ->
cleanUp(data)
}
}
Expand All @@ -219,13 +232,14 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaUploadInfo: MediaUploadInfo,
caption: String?,
sendActionState: MutableState<SendActionState>,
dismissAfterSend: Boolean,
) = runCatching {
val context = coroutineContext
val progressCallback = object : ProgressCallback {
override fun onProgress(current: Long, total: Long) {
// Note will not happen if useSendQueue is true
if (context.isActive) {
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat())
sendActionState.value = SendActionState.Sending.Uploading(current.toFloat() / total.toFloat(), mediaUploadInfo)
}
}
}
Expand All @@ -240,14 +254,17 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
cleanUp(mediaUploadInfo)
// Reset the sendActionState to ensure that dialog is closed before the screen
sendActionState.value = SendActionState.Done
onDoneListener()

if (dismissAfterSend) {
onDoneListener()
}
},
onFailure = { error ->
Timber.e(error, "Failed to send attachment")
if (error is CancellationException) {
throw error
} else {
sendActionState.value = SendActionState.Failure(error)
sendActionState.value = SendActionState.Failure(error, mediaUploadInfo)
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package io.element.android.features.messages.impl.attachments.preview

import androidx.compose.runtime.Immutable
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.textcomposer.model.TextEditorState

data class AttachmentsPreviewState(
Expand All @@ -26,11 +27,18 @@ sealed interface SendActionState {

@Immutable
sealed interface Sending : SendActionState {
data object InstantSending : Sending
data object Processing : Sending
data class Uploading(val progress: Float) : Sending
data class Processing(val displayProgress: Boolean) : Sending
data class ReadyToUpload(val mediaInfo: MediaUploadInfo) : Sending
data class Uploading(val progress: Float, val mediaUploadInfo: MediaUploadInfo) : Sending
}

data class Failure(val error: Throwable) : SendActionState
data class Failure(val error: Throwable, val mediaUploadInfo: MediaUploadInfo?) : SendActionState
data object Done : SendActionState

fun mediaUploadInfo(): MediaUploadInfo? = when (this) {
is Sending.ReadyToUpload -> mediaInfo
is Sending.Uploading -> mediaUploadInfo
is Failure -> mediaUploadInfo
else -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,25 @@ package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.core.net.toUri
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.textcomposer.model.TextEditorState
import io.element.android.libraries.textcomposer.model.aTextEditorStateMarkdown
import java.io.File

open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
override val values: Sequence<AttachmentsPreviewState>
get() = sequenceOf(
anAttachmentsPreviewState(),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f)),
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"))),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = false)),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Processing(displayProgress = true)),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.ReadyToUpload(aMediaUploadInfo())),
anAttachmentsPreviewState(sendActionState = SendActionState.Sending.Uploading(0.5f, aMediaUploadInfo())),
anAttachmentsPreviewState(sendActionState = SendActionState.Failure(RuntimeException("error"), aMediaUploadInfo())),
anAttachmentsPreviewState(allowCaption = false),
anAttachmentsPreviewState(showCaptionCompatibilityWarning = true),
)
Expand All @@ -44,3 +50,20 @@ fun anAttachmentsPreviewState(
showCaptionCompatibilityWarning = showCaptionCompatibilityWarning,
eventSink = {}
)

fun aMediaUploadInfo(
filePath: String = "file://path",
thumbnailFilePath: String? = null,
) = MediaUploadInfo.Image(
file = File(filePath),
imageInfo = ImageInfo(
height = 100,
width = 100,
mimetype = MimeTypes.Jpeg,
size = 1000,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,
),
thumbnailFile = thumbnailFilePath?.let { File(it) },
)
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ fun AttachmentsPreviewView(
}

fun postCancel() {
state.eventSink(AttachmentsPreviewEvents.Cancel)
state.eventSink(AttachmentsPreviewEvents.CancelAndDismiss)
}

fun postClearSendState() {
state.eventSink(AttachmentsPreviewEvents.ClearSendState)
state.eventSink(AttachmentsPreviewEvents.CancelAndClearSendState)
}

BackHandler(enabled = state.sendActionState !is SendActionState.Sending) {
Expand Down Expand Up @@ -106,12 +106,14 @@ private fun AttachmentSendStateView(
) {
when (sendActionState) {
is SendActionState.Sending.Processing -> {
ProgressDialog(
type = ProgressDialogType.Indeterminate,
text = stringResource(id = CommonStrings.common_sending),
showCancelButton = true,
onDismissRequest = onDismissClick,
)
if (sendActionState.displayProgress) {
ProgressDialog(
type = ProgressDialogType.Indeterminate,
text = stringResource(id = CommonStrings.common_sending),
showCancelButton = true,
onDismissRequest = onDismissClick,
)
}
}
is SendActionState.Sending.Uploading -> {
ProgressDialog(
Expand Down
Loading

0 comments on commit 3e0f87b

Please sign in to comment.