From e322ba1b3205cbd1fe426ebca80693664b76fd5f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 5 Jun 2023 20:52:17 +0200 Subject: [PATCH] Media: handle openWith and share actions (need to inject context for tests...). Also introduce MediaInfo --- .../messages/impl/MessagesFlowNode.kt | 28 ++++--- .../preview/AttachmentsPreviewPresenter.kt | 2 +- .../AttachmentsPreviewStateProvider.kt | 4 +- .../impl/media/helper/fileExtensionAndSize.kt | 28 +++++++ .../media/local/AndroidLocalMediaActions.kt | 44 +++++++--- .../media/local/AndroidLocalMediaFactory.kt | 23 ++++-- .../messages/impl/media/local/LocalMedia.kt | 9 +-- .../impl/media/local/LocalMediaActions.kt | 10 ++- .../impl/media/local/LocalMediaFactory.kt | 11 ++- .../impl/media/local/LocalMediaView.kt | 77 +++++++++++++++++- .../messages/impl/media/local/MediaInfo.kt | 45 +++++++++++ .../impl/media/viewer/MediaViewerEvents.kt | 1 + .../impl/media/viewer/MediaViewerNode.kt | 4 +- .../impl/media/viewer/MediaViewerPresenter.kt | 49 ++++++++--- .../impl/media/viewer/MediaViewerState.kt | 4 +- .../media/viewer/MediaViewerStateProvider.kt | 36 ++++++--- .../impl/media/viewer/MediaViewerView.kt | 81 +++++++++++-------- .../MessageComposerPresenter.kt | 15 ++-- .../TimelineItemContentMessageFactory.kt | 17 ++-- .../model/event/TimelineItemFileContent.kt | 14 +--- .../model/event/TimelineItemImageContent.kt | 3 +- .../event/TimelineItemImageContentProvider.kt | 3 +- .../model/event/TimelineItemVideoContent.kt | 3 +- .../event/TimelineItemVideoContentProvider.kt | 4 +- .../features/messages/fixtures/media.kt | 10 ++- 25 files changed, 395 insertions(+), 130 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 3dc77bc00a..b5d3c40cf8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -32,6 +32,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode +import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.viewer.MediaViewerNode import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -64,10 +65,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class MediaViewer( - val title: String, + val mediaInfo: MediaInfo, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, - val mimeType: String?, ) : NavTarget @Parcelize @@ -100,10 +100,9 @@ class MessagesFlowNode @AssistedInject constructor( } is NavTarget.MediaViewer -> { val inputs = MediaViewerNode.Inputs( - name = navTarget.title, + mediaInfo = navTarget.mediaInfo, mediaSource = navTarget.mediaSource, thumbnailSource = navTarget.thumbnailSource, - mimeType = navTarget.mimeType, ) createNode(buildContext, listOf(inputs)) } @@ -118,30 +117,39 @@ class MessagesFlowNode @AssistedInject constructor( when (event.content) { is TimelineItemImageContent -> { val navTarget = NavTarget.MediaViewer( - title = event.content.body, + mediaInfo = MediaInfo( + name = event.content.body, + mimeType = event.content.mimeType, + formattedFileSize = event.content.formattedFileSize + ), mediaSource = event.content.mediaSource, thumbnailSource = event.content.mediaSource, - mimeType = event.content.mimeType ) backstack.push(navTarget) } is TimelineItemVideoContent -> { val mediaSource = event.content.videoSource val navTarget = NavTarget.MediaViewer( - title = event.content.body, + mediaInfo = MediaInfo( + name = event.content.body, + mimeType = event.content.mimeType, + formattedFileSize = event.content.formattedFileSize + ), mediaSource = mediaSource, thumbnailSource = event.content.thumbnailSource, - mimeType = event.content.mimeType, ) backstack.push(navTarget) } is TimelineItemFileContent -> { val mediaSource = event.content.fileSource val navTarget = NavTarget.MediaViewer( - title = event.content.body, + mediaInfo = MediaInfo( + name = event.content.body, + mimeType = event.content.mimeType, + formattedFileSize = event.content.formattedFileSize + ), mediaSource = mediaSource, thumbnailSource = event.content.thumbnailSource, - mimeType = event.content.mimeType, ) backstack.push(navTarget) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index d80359e88c..807a23f6cc 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -84,7 +84,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( sendActionState: MutableState>, ) { suspend { - mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.mimeType, mediaAttachment.compressIfPossible) + mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.info.mimeType, mediaAttachment.compressIfPossible) }.executeResult(sendActionState) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 26565a226a..3724bb7f68 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -20,8 +20,8 @@ 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.features.messages.impl.media.local.LocalMedia +import io.element.android.features.messages.impl.media.local.anImageInfo import io.element.android.libraries.architecture.Async -import io.element.android.libraries.core.mimetype.MimeTypes open class AttachmentsPreviewStateProvider : PreviewParameterProvider { override val values: Sequence @@ -34,7 +34,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider = Async.Uninitialized) = AttachmentsPreviewState( attachment = Attachment.Media( - localMedia = LocalMedia("path".toUri(), MimeTypes.Jpeg, "an image", 1000L), + localMedia = LocalMedia("path".toUri(), anImageInfo()), compressIfPossible = true ), sendActionState = sendActionState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt new file mode 100644 index 0000000000..43d6ba2a9b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/helper/fileExtensionAndSize.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.media.helper + +fun formatFileExtensionAndSize(name: String, size: String?): String { + val fileExtension = name.substringAfterLast('.', "").uppercase() + return buildString { + append(fileExtension) + if (size != null) { + append(' ') + append("($size)") + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt index 7b10391994..71bb2ae31c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaActions.kt @@ -20,6 +20,7 @@ import android.content.ContentResolver import android.content.ContentValues import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore @@ -60,16 +61,17 @@ class AndroidLocalMediaActions @Inject constructor( } } - override suspend fun share(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { + override suspend fun share(activityContext: Context, localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) runCatching { - val authority = "${buildMeta.applicationId}.fileprovider" - val uriFromFileProvider = FileProvider.getUriForFile(context, authority, localMedia.toFile()) - val shareMediaIntent = Intent(Intent.ACTION_VIEW) - .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - .setDataAndType(uriFromFileProvider, localMedia.mimeType) + val shareableUri = localMedia.toShareableUri() + val shareMediaIntent = Intent(Intent.ACTION_SEND) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .putExtra(Intent.EXTRA_STREAM, shareableUri) + .setTypeAndNormalize(localMedia.info.mimeType) withContext(coroutineDispatchers.main) { - context.startActivity(shareMediaIntent, null) + val intent = Intent.createChooser(shareMediaIntent, null) + activityContext.startActivity(intent) } }.onSuccess { Timber.v("Share media succeed") @@ -78,11 +80,33 @@ class AndroidLocalMediaActions @Inject constructor( } } + override suspend fun open(activityContext: Context, localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { + require(localMedia.uri.scheme == ContentResolver.SCHEME_FILE) + runCatching { + val openMediaIntent = Intent(Intent.ACTION_VIEW) + .setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setDataAndType(localMedia.toShareableUri(), localMedia.info.mimeType) + withContext(coroutineDispatchers.main) { + activityContext.startActivity(openMediaIntent) + } + }.onSuccess { + Timber.v("Open media succeed") + }.onFailure { + Timber.e(it, "Open media failed") + } + } + + private fun LocalMedia.toShareableUri(): Uri { + val mediaAsFile = this.toFile() + val authority = "${buildMeta.applicationId}.fileprovider" + return FileProvider.getUriForFile(context, authority, mediaAsFile).normalizeScheme() + } + @RequiresApi(Build.VERSION_CODES.Q) private fun saveOnDiskUsingMediaStore(localMedia: LocalMedia) { val contentValues = ContentValues().apply { - put(MediaStore.MediaColumns.DISPLAY_NAME, localMedia.name) - put(MediaStore.MediaColumns.MIME_TYPE, localMedia.mimeType) + put(MediaStore.MediaColumns.DISPLAY_NAME, localMedia.info.name) + put(MediaStore.MediaColumns.MIME_TYPE, localMedia.info.mimeType) put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } val resolver = context.contentResolver @@ -99,7 +123,7 @@ class AndroidLocalMediaActions @Inject constructor( private fun saveOnDiskUsingExternalStorageApi(localMedia: LocalMedia) { val target = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - localMedia.name ?: "" + localMedia.info.name ) localMedia.openStream()?.use { input -> FileOutputStream(target).use { output -> diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt index 4908d79f68..ac22785277 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.media.local import android.content.Context import android.net.Uri import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileSize import io.element.android.libraries.androidutils.file.getMimeType @@ -29,18 +30,26 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class AndroidLocalMediaFactory @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val fileSizeFormatter: FileSizeFormatter, ) : LocalMediaFactory { - override fun createFromUri(uri: Uri, mimeType: String?, name: String?): LocalMedia { + override fun createFromUri( + uri: Uri, + mimeType: String?, + name: String?, + formattedFileSize: String? + ): LocalMedia { val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream - val fileName = name ?: context.getFileName(uri) - val fileSize = context.getFileSize(uri) + val fileName = name ?: context.getFileName(uri) ?: "" + val fileSize = formattedFileSize ?: fileSizeFormatter.format(context.getFileSize(uri)) return LocalMedia( uri = uri, - mimeType = resolvedMimeType, - name = fileName, - size = fileSize + info = MediaInfo( + mimeType = resolvedMimeType, + name = fileName, + formattedFileSize = fileSize + ) ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index e125e531bf..f5a09def47 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -19,14 +19,13 @@ package io.element.android.features.messages.impl.media.local import android.net.Uri import android.os.Parcelable import androidx.compose.runtime.Immutable -import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize @Immutable data class LocalMedia( val uri: Uri, - val mimeType: String, - val name: String?, - val size: Long, -) : Parcelable + val info: MediaInfo, +) : Parcelable { + +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt index 03f09488df..23c029c09f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaActions.kt @@ -16,6 +16,8 @@ package io.element.android.features.messages.impl.media.local +import android.content.Context + interface LocalMediaActions { /** * Will save the current media to the Downloads directory. @@ -27,6 +29,12 @@ interface LocalMediaActions { * Will try to find a suitable application to share the media with. * The [LocalMedia.uri] needs to have a file scheme. */ - suspend fun share(localMedia: LocalMedia): Result + suspend fun share(activityContext: Context, localMedia: LocalMedia): Result + + /** + * Will try to find a suitable application to open the media with. + * The [LocalMedia.uri] needs to have a file scheme. + */ + suspend fun open(activityContext: Context, localMedia: LocalMedia): Result } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt index 917369292a..c90e9d43ed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaFactory.kt @@ -24,22 +24,21 @@ import io.element.android.libraries.matrix.api.media.toFile interface LocalMediaFactory { /** - * This method will create a [LocalMedia] with the given [uri] and [mimeType] - * If the [mimeType] is null, it'll try to read it from the content. - * If the [name] is null, it'll try to read it from the content. + * This method will create a [LocalMedia] with the given mimeType, name and formattedFileSize + * If any of those params are null, it'll try to read them from the content. */ fun createFromUri( uri: Uri, mimeType: String?, name: String?, + formattedFileSize: String? ): LocalMedia } fun LocalMediaFactory.createFromMediaFile( mediaFile: MediaFile, - mimeType: String?, - name: String? + mediaInfo: MediaInfo, ): LocalMedia { val uri = mediaFile.toFile().toUri() - return createFromUri(uri = uri, mimeType = mimeType, name = name) + return createFromUri(uri = uri, mimeType = mediaInfo.mimeType, name = mediaInfo.name, formattedFileSize = mediaInfo.formattedFileSize) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 28b49cf8ec..502e10e0a4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -17,18 +17,35 @@ package io.element.android.features.messages.impl.media.local import android.annotation.SuppressLint +import android.net.Uri import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Attachment +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem @@ -36,6 +53,7 @@ import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView +import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper import io.element.android.features.messages.impl.media.local.pdf.PdfViewer import io.element.android.features.messages.impl.media.local.pdf.rememberPdfViewerState @@ -43,6 +61,8 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.designsystem.R +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.ZoomableState @@ -55,12 +75,13 @@ import me.saket.telephoto.zoomable.rememberZoomableState fun LocalMediaView( localMedia: LocalMedia?, modifier: Modifier = Modifier, - mimeType: String? = localMedia?.mimeType, + info: MediaInfo? = localMedia?.info, onReady: () -> Unit = {}, ) { val zoomableState = rememberZoomableState( zoomSpec = ZoomSpec(maxZoomFactor = 5f) ) + val mimeType = info?.mimeType when { mimeType.isMimeTypeImage() -> MediaImageView( localMedia = localMedia, @@ -79,7 +100,12 @@ fun LocalMediaView( onReady = onReady, modifier = modifier ) - else -> Unit + else -> MediaFileView( + uri = localMedia?.uri, + info = info, + onReady = onReady, + modifier = modifier + ) } } @@ -186,3 +212,50 @@ fun MediaPDFView( } PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier) } + +@Composable +fun MediaFileView( + uri: Uri?, + info: MediaInfo?, + onReady: () -> Unit, + modifier: Modifier = Modifier, +) { + LaunchedEffect(Unit) { + if(uri != null) { + onReady() + } + } + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + modifier = Modifier + .size(72.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onBackground), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.Attachment, + contentDescription = "OpenFile", + tint = MaterialTheme.colorScheme.background, + modifier = Modifier + .size(32.dp) + .rotate(-45f), + ) + } + if(info == null) return + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = info.name, + maxLines = 2, + fontSize = 16.sp, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = formatFileExtensionAndSize(info.name, info.formattedFileSize), + fontSize = 14.sp, + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt new file mode 100644 index 0000000000..005da816cc --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.media.local + +import android.os.Parcelable +import io.element.android.libraries.core.mimetype.MimeTypes +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MediaInfo( + val name: String, + val mimeType: String, + val formattedFileSize: String, +) : Parcelable + + +fun anImageInfo(): MediaInfo = MediaInfo( + "an image file", MimeTypes.Jpeg, "4MB" +) + +fun aVideoInfo(): MediaInfo = MediaInfo( + "a video file", MimeTypes.Mp4, "14MB" +) + +fun aPdfInfo(): MediaInfo = MediaInfo( + "a pdf file", MimeTypes.Pdf, "23MB" +) + +fun aFileInfo(): MediaInfo = MediaInfo( + "an apk file", MimeTypes.Apk, "50MB" +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt index 375b7e4a34..b680ee58c9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.media.viewer sealed interface MediaViewerEvents { object SaveOnDisk: MediaViewerEvents object Share: MediaViewerEvents + object OpenWith: MediaViewerEvents object RetryLoading : MediaViewerEvents object ClearLoadingError : MediaViewerEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt index ee1bb50ce9..32bf2e39ac 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt @@ -24,6 +24,7 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.theme.ForcedDarkElementTheme @@ -38,10 +39,9 @@ class MediaViewerNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { data class Inputs( - val name: String, + val mediaInfo: MediaInfo, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, - val mimeType: String? ) : NodeInputs private val inputs: Inputs = inputs() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt index faa86dac71..7aa2c0bd9b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt @@ -16,6 +16,8 @@ package io.element.android.features.messages.impl.media.viewer +import android.content.ActivityNotFoundException +import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState @@ -24,6 +26,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -40,6 +43,7 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import io.element.android.libraries.androidutils.R as UtilsR import io.element.android.libraries.ui.strings.R as StringR class MediaViewerPresenter @AssistedInject constructor( @@ -65,6 +69,7 @@ class MediaViewerPresenter @AssistedInject constructor( val localMedia: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + val context = LocalContext.current val snackbarMessage = handleSnackbarMessage(snackbarDispatcher) DisposableEffect(loadMediaTrigger) { coroutineScope.downloadMedia(mediaFile, localMedia) @@ -78,13 +83,13 @@ class MediaViewerPresenter @AssistedInject constructor( MediaViewerEvents.RetryLoading -> loadMediaTrigger++ MediaViewerEvents.ClearLoadingError -> localMedia.value = Async.Uninitialized MediaViewerEvents.SaveOnDisk -> coroutineScope.saveOnDisk(localMedia.value) - MediaViewerEvents.Share -> coroutineScope.share(localMedia.value) + MediaViewerEvents.Share -> coroutineScope.share(context, localMedia.value) + MediaViewerEvents.OpenWith -> coroutineScope.open(context, localMedia.value) } } return MediaViewerState( - name = inputs.name, - mimeType = inputs.mimeType, + mediaInfo = inputs.mediaInfo, thumbnailSource = inputs.thumbnailSource, downloadedMedia = localMedia.value, snackbarMessage = snackbarMessage, @@ -96,16 +101,15 @@ class MediaViewerPresenter @AssistedInject constructor( localMedia.value = Async.Loading() mediaLoader.downloadMediaFile( source = inputs.mediaSource, - mimeType = inputs.mimeType, - body = inputs.name + mimeType = inputs.mediaInfo.mimeType, + body = inputs.mediaInfo.name ) .onSuccess { mediaFile.value = it }.mapCatching { mediaFile -> localMediaFactory.createFromMediaFile( mediaFile = mediaFile, - mimeType = inputs.mimeType, - name = inputs.name + mediaInfo = inputs.mediaInfo ) }.onSuccess { localMedia.value = Async.Success(it) @@ -127,12 +131,39 @@ class MediaViewerPresenter @AssistedInject constructor( } } - private fun CoroutineScope.share(localMedia: Async) = launch { + private fun CoroutineScope.share(activityContext: Context, localMedia: Async) = launch { when (localMedia) { - is Async.Success -> mediaActionsHandler.share(localMedia.state) + is Async.Success -> { + mediaActionsHandler.share(activityContext, localMedia.state) + .onFailure { + val snackbarMessage = SnackbarMessage(openShareError(it)) + snackbarDispatcher.post(snackbarMessage) + } + } else -> Unit } } + + private fun CoroutineScope.open(activityContext: Context, localMedia: Async) = launch { + when (localMedia) { + is Async.Success -> { + mediaActionsHandler.open(activityContext, localMedia.state) + .onFailure { + val snackbarMessage = SnackbarMessage(openShareError(it)) + snackbarDispatcher.post(snackbarMessage) + } + } + else -> Unit + } + } + + private fun openShareError(throwable: Throwable): Int { + return if (throwable is ActivityNotFoundException) { + UtilsR.string.error_no_compatible_app_found + } else { + StringR.string.error_unknown + } + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt index 77950a4bf3..18375746c5 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt @@ -17,13 +17,13 @@ package io.element.android.features.messages.impl.media.viewer import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.media.MediaSource data class MediaViewerState( - val name: String, - val mimeType: String?, + val mediaInfo: MediaInfo, val thumbnailSource: MediaSource?, val downloadedMedia: Async, val snackbarMessage: SnackbarMessage?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 4f12be9ba9..2705072f0d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -18,8 +18,12 @@ package io.element.android.features.messages.impl.media.viewer import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.media3.common.MimeTypes import io.element.android.features.messages.impl.media.local.LocalMedia +import io.element.android.features.messages.impl.media.local.MediaInfo +import io.element.android.features.messages.impl.media.local.aFileInfo +import io.element.android.features.messages.impl.media.local.aPdfInfo +import io.element.android.features.messages.impl.media.local.aVideoInfo +import io.element.android.features.messages.impl.media.local.anImageInfo import io.element.android.libraries.architecture.Async open class MediaViewerStateProvider : PreviewParameterProvider { @@ -30,24 +34,36 @@ open class MediaViewerStateProvider : PreviewParameterProvider aMediaViewerState(Async.Failure(IllegalStateException())), aMediaViewerState( Async.Success( - LocalMedia( - Uri.EMPTY, MimeTypes.IMAGE_JPEG, "an image file", 100L - ) + LocalMedia(Uri.EMPTY, anImageInfo()) ), + anImageInfo(), ), aMediaViewerState( Async.Success( - LocalMedia( - Uri.EMPTY, MimeTypes.VIDEO_MP4, "a video file", 100L - ) + LocalMedia(Uri.EMPTY, aVideoInfo()) ), + aVideoInfo(), + ), + aMediaViewerState( + Async.Success( + LocalMedia(Uri.EMPTY, aPdfInfo()) + ), + aPdfInfo(), + ), + aMediaViewerState( + Async.Success( + LocalMedia(Uri.EMPTY, aFileInfo()) + ), + aFileInfo(), ) ) } -fun aMediaViewerState(downloadedMedia: Async = Async.Uninitialized) = MediaViewerState( - name = "A media", - mimeType = MimeTypes.IMAGE_JPEG, +fun aMediaViewerState( + downloadedMedia: Async = Async.Uninitialized, + mediaInfo: MediaInfo = anImageInfo(), +) = MediaViewerState( + mediaInfo = mediaInfo, thumbnailSource = null, downloadedMedia = downloadedMedia, snackbarMessage = null diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index 38b7628626..3373b56726 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -22,12 +22,18 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost @@ -43,15 +49,14 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.element.android.features.messages.impl.media.local.LocalMediaView import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.RetryDialog -import io.element.android.libraries.designsystem.modifiers.roundedBackground import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.IconButton import io.element.android.libraries.designsystem.theme.components.Scaffold @@ -102,7 +107,8 @@ fun MediaViewerView( val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage) - Scaffold(modifier, + Scaffold( + modifier, topBar = { MediaViewerTopBar( actionsEnabled = state.downloadedMedia is Async.Success, @@ -120,36 +126,48 @@ fun MediaViewerView( } }, ) { - Box( + Column( modifier = Modifier .fillMaxSize() .padding(it), - contentAlignment = Alignment.Center ) { - if (state.downloadedMedia is Async.Failure) { - ErrorView( - errorMessage = stringResource(id = StringR.string.error_unknown), - onRetry = ::onRetry, - onDismiss = ::onDismissError + if (showProgress) { + LinearProgressIndicator( + Modifier + .fillMaxWidth() + .height(2.dp) + ) + } else { + Spacer(Modifier.height(2.dp)) + } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + if (state.downloadedMedia is Async.Failure) { + ErrorView( + errorMessage = stringResource(id = StringR.string.error_unknown), + onRetry = ::onRetry, + onDismiss = ::onDismissError + ) + } + LocalMediaView( + localMedia = state.downloadedMedia.dataOrNull(), + info = state.mediaInfo, + onReady = ::onMediaReady + ) + ThumbnailView( + thumbnailSource = state.thumbnailSource, + showThumbnail = showThumbnail, ) } - LocalMediaView( - localMedia = state.downloadedMedia.dataOrNull(), - mimeType = state.mimeType, - onReady = ::onMediaReady - ) - ThumbnailView( - thumbnailSource = state.thumbnailSource, - showThumbnail = showThumbnail, - showProgress = showProgress, - ) } } } @Composable private fun MediaViewerTopBar( - actionsEnabled : Boolean, + actionsEnabled: Boolean, onBackPressed: () -> Unit, eventSink: (MediaViewerEvents) -> Unit, ) { @@ -160,10 +178,10 @@ private fun MediaViewerTopBar( IconButton( enabled = actionsEnabled, onClick = { - eventSink(MediaViewerEvents.Share) + eventSink(MediaViewerEvents.OpenWith) }, ) { - Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(id = string.action_share)) + Icon(imageVector = Icons.Default.OpenInNew, contentDescription = stringResource(id = string.action_share)) } IconButton( enabled = actionsEnabled, @@ -173,6 +191,14 @@ private fun MediaViewerTopBar( ) { Icon(imageVector = Icons.Default.Download, contentDescription = stringResource(id = string.action_save)) } + IconButton( + enabled = actionsEnabled, + onClick = { + eventSink(MediaViewerEvents.Share) + }, + ) { + Icon(imageVector = Icons.Default.Share, contentDescription = stringResource(id = string.action_share)) + } } ) } @@ -181,7 +207,6 @@ private fun MediaViewerTopBar( private fun ThumbnailView( thumbnailSource: MediaSource?, showThumbnail: Boolean, - showProgress: Boolean, ) { AnimatedVisibility( visible = showThumbnail, @@ -203,14 +228,6 @@ private fun ThumbnailView( contentScale = ContentScale.Fit, contentDescription = null, ) - if (showProgress) { - Box( - modifier = Modifier.roundedBackground(), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index cb966189a0..7c5e2d5705 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -192,7 +192,7 @@ class MessageComposerPresenter @Inject constructor( is Attachment.Media -> { sendMedia( uri = attachment.localMedia.uri, - mimeType = attachment.localMedia.mimeType, + mimeType = attachment.localMedia.info.mimeType, attachmentState = attachmentState ) } @@ -210,12 +210,17 @@ class MessageComposerPresenter @Inject constructor( attachmentsState.value = AttachmentsState.None return } - val localMedia = localMediaFactory.createFromUri(uri, mimeType, null) + val localMedia = localMediaFactory.createFromUri( + uri = uri, + mimeType = mimeType, + name = null, + formattedFileSize = null + ) val mediaAttachment = Attachment.Media(localMedia, compressIfPossible) val isPreviewable = when { - MimeTypes.isImage(localMedia.mimeType) -> true - MimeTypes.isVideo(localMedia.mimeType) -> true - MimeTypes.isAudio(localMedia.mimeType) -> true + MimeTypes.isImage(localMedia.info.mimeType) -> true + MimeTypes.isVideo(localMedia.info.mimeType) -> true + MimeTypes.isAudio(localMedia.info.mimeType) -> true else -> false } attachmentsState.value = if (isPreviewable) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index cc8082be62..8941878334 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.features.messages.impl.timeline.util.FileSizeFormatter import io.element.android.features.messages.impl.timeline.util.toHtmlDocument +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType @@ -51,11 +52,12 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemImageContent( body = messageType.body, mediaSource = messageType.source, - mimeType = messageType.info?.mimetype, + mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, blurhash = messageType.info?.blurhash, width = messageType.info?.width?.toInt(), height = messageType.info?.height?.toInt(), - aspectRatio = aspectRatio + aspectRatio = aspectRatio, + formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0) ) } is VideoMessageType -> { @@ -64,22 +66,21 @@ class TimelineItemContentMessageFactory @Inject constructor( body = messageType.body, thumbnailSource = messageType.info?.thumbnailSource, videoSource = messageType.source, - mimeType = messageType.info?.mimetype, + mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, width = messageType.info?.width?.toInt(), height = messageType.info?.height?.toInt(), duration = messageType.info?.duration ?: 0L, blurHash = messageType.info?.blurhash, - aspectRatio = aspectRatio + aspectRatio = aspectRatio, + formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0) ) } is FileMessageType -> TimelineItemFileContent( body = messageType.body, thumbnailSource = messageType.info?.thumbnailSource, fileSource = messageType.source, - mimeType = messageType.info?.mimetype, - formattedFileSize = messageType.info?.size?.let { - fileSizeFormatter.format(it) - }, + mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, + formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0) ) is NoticeMessageType -> TimelineItemNoticeContent( body = messageType.body, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt index 9307cf8a67..197bae2dda 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt @@ -16,23 +16,17 @@ package io.element.android.features.messages.impl.timeline.model.event +import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize import io.element.android.libraries.matrix.api.media.MediaSource data class TimelineItemFileContent( val body: String, val fileSource: MediaSource, val thumbnailSource: MediaSource?, - val formattedFileSize: String?, - val mimeType: String?, + val formattedFileSize: String, + val mimeType: String, ) : TimelineItemEventContent { override val type: String = "TimelineItemFileContent" - private val fileExtension = body.substringAfterLast('.', "").uppercase() - val fileExtensionAndSize = buildString { - append(fileExtension) - if (formattedFileSize != null) { - append(' ') - append("($formattedFileSize)") - } - } + val fileExtensionAndSize = formatFileExtensionAndSize(body, formattedFileSize) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 850dc9782c..18be8d404d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -21,7 +21,8 @@ import io.element.android.libraries.matrix.api.media.MediaSource data class TimelineItemImageContent( val body: String, val mediaSource: MediaSource, - val mimeType: String?, + val formattedFileSize: String, + val mimeType: String, val blurhash: String?, val width: Int?, val height: Int?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index 97bbf6ed41..66b35a07d3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -36,5 +36,6 @@ fun aTimelineItemImageContent() = TimelineItemImageContent( blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", width = null, height = 300, - aspectRatio = 0.5f + aspectRatio = 0.5f, + formattedFileSize = "4MB" ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt index 41fba2a29f..1432ebbda0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt @@ -27,7 +27,8 @@ data class TimelineItemVideoContent( val blurHash: String?, val height: Int?, val width: Int?, - val mimeType: String?, + val mimeType: String, + val formattedFileSize: String, ) : TimelineItemEventContent { override val type: String = "TimelineItemImageContent" } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt index 9751bb4067..937f35b349 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.model.event import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource open class TimelineItemVideoContentProvider : PreviewParameterProvider { @@ -37,5 +38,6 @@ fun aTimelineItemVideoContent() = TimelineItemVideoContent( videoSource = MediaSource(""), height = 300, width = 150, - mimeType = null + mimeType = MimeTypes.Mp4, + formattedFileSize = "14MB" ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt index 60a01a76d4..7d5bb79b4a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt @@ -20,7 +20,7 @@ import android.net.Uri import androidx.media3.common.MimeTypes import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMedia -import io.mockk.mockk +import io.element.android.features.messages.impl.media.local.MediaInfo fun aLocalMedia( uri: Uri, @@ -29,9 +29,11 @@ fun aLocalMedia( size: Long = 1000, ) = LocalMedia( uri = uri, - mimeType = mimeType, - name = name, - size = size, + info = MediaInfo( + mimeType = mimeType, + name = name, + formattedFileSize = "${size}B", + ) ) fun aMediaAttachment(localMedia: LocalMedia, compressIfPossible: Boolean = true) = Attachment.Media(