Merge branch 'develop' into fix/6371-save-file-name-conflicts

This commit is contained in:
Benoit Marty
2026-08-24 09:38:35 +02:00
committed by GitHub
1451 changed files with 19439 additions and 7920 deletions
@@ -12,9 +12,15 @@ import android.net.Uri
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.mediaviewer.api.MediaInfo
/**
* Builds the [LocalMedia] the media viewer renders, from either a downloaded Matrix media file or an arbitrary content uri.
*/
interface LocalMediaFactory {
/**
* This method will create a [LocalMedia] with the given [MediaFile] and [MediaInfo].
*
* @param mediaFile the file already downloaded from the homeserver.
* @param mediaInfo what is known about the media from the event that carried it.
*/
fun createFromMediaFile(
mediaFile: MediaFile,
@@ -24,6 +30,11 @@ interface LocalMediaFactory {
/**
* 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.
*
* @param uri the content to display, for instance one just picked by the user.
* @param mimeType the MIME type, or `null` to read it from the content.
* @param name the file name, or `null` to read it from the content.
* @param formattedFileSize the size to display, or `null` to read it from the content.
*/
fun createFromUri(
uri: Uri,
@@ -10,7 +10,15 @@ package io.element.android.libraries.mediaviewer.api.local
import androidx.compose.runtime.Composable
/**
* Renders a media file held on the device, picking the right player or viewer for its type.
*/
interface LocalMediaRenderer {
/**
* Draws the media, filling the space it is given.
*
* @param localMedia the file to display, along with what is known about its type.
*/
@Composable
fun Render(localMedia: LocalMedia)
}
@@ -8,6 +8,13 @@
package io.element.android.libraries.mediaviewer.api.util
/**
* Extracts the extension of a file name, so it can be shown to the user and used to pick an icon.
*/
interface FileExtensionExtractor {
/**
* @param name the file name to inspect.
* @return the extension without its dot, or an empty string when the name has none.
*/
fun extractFromName(name: String): String
}
@@ -83,7 +83,9 @@ class TimelineMediaGalleryDataSource(
},
{
Timber.e(it, "Failed to get media timeline for room ${room.roomId}")
groupedMediaItemsFlow.emit(AsyncData.Failure(it))
if (cache == null) {
groupedMediaItemsFlow.emit(AsyncData.Failure(it))
}
},
)
}.flatMapLatest { timeline ->
@@ -266,4 +266,5 @@ class ExoPlayerForPreview(
override fun setVideoCodecParameters(codecParameters: CodecParameters) {}
override fun addVideoCodecParametersChangeListener(listener: CodecParametersChangeListener, keys: List<String>) {}
override fun removeVideoCodecParametersChangeListener(listener: CodecParametersChangeListener) {}
override fun setEnforceAdPlaybackOnTimelineRefresh(enforceAdPlaybackOnTimelineRefresh: Boolean) = throw NotImplementedError()
}
@@ -92,7 +92,7 @@ class MediaViewerNode(
is MediaViewerEntryPoint.Params.RoomMedia -> {
val eventId = inputs.eventId
if (eventId == null) {
// Should not happen
// Local echo, the viewer will not be focused on the item
timelineMediaGalleryDataSource
} else {
// Can we use a specific timeline?
@@ -82,7 +82,6 @@ import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.async.AsyncFailure
import io.element.android.libraries.designsystem.components.async.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncLoading
import io.element.android.libraries.designsystem.components.button.BackButton
@@ -209,7 +208,6 @@ fun MediaViewerView(
when (val dataForPage = state.listData[page]) {
is MediaViewerPageData.Failure -> {
MediaViewerErrorPage(
throwable = dataForPage.throwable,
onDismiss = onBackClick,
)
}
@@ -414,6 +412,7 @@ private fun MediaViewerPage(
if (showProgress) {
LinearProgressIndicator(
modifier = Modifier
.padding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top).asPaddingValues())
.fillMaxWidth()
.height(2.dp)
)
@@ -506,7 +505,6 @@ private fun MediaViewerLoadingPage(
@Composable
private fun MediaViewerErrorPage(
throwable: Throwable,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -520,9 +518,10 @@ private fun MediaViewerErrorPage(
.navigationBarsPadding(),
contentAlignment = Alignment.Center
) {
AsyncFailure(
throwable = throwable,
onRetry = null
IconTitleSubtitleMolecule(
iconStyle = BigIcon.Style.AlertSolid,
title = stringResource(CommonStrings.common_something_went_wrong),
subTitle = stringResource(CommonStrings.common_something_went_wrong_message),
)
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_media_browser_delete_confirmation_subtitle">"ستتم إزالة هذا الملف من الغرفة ولن يتمكن الأعضاء من الوصول إليه."</string>
<string name="screen_media_browser_delete_confirmation_title">"إزالة الملف؟"</string>
<string name="screen_media_browser_download_error_message">"تحقّق من اتصالك بالإنترنت وحاول مرة أخرى."</string>
<string name="screen_media_browser_files_empty_state_subtitle">"ستظهر هنا المستندات والملفات الصوتية والرسائل الصوتية التي يتم رفعها إلى هذه الغرفة."</string>
<string name="screen_media_browser_files_empty_state_title">"لم يتم رفع أي ملفات بعد"</string>
<string name="screen_media_browser_list_loading_files">"جارٍ تحميل الملفات…"</string>
<string name="screen_media_browser_list_loading_media">"جارٍ تحميل الوسائط…"</string>
<string name="screen_media_browser_list_mode_files">"الملفات"</string>
<string name="screen_media_browser_list_mode_media">"الوسائط"</string>
<string name="screen_media_browser_media_empty_state_subtitle">"ستظهر هنا الصور ومقاطع الفيديو التي يتم رفعها إلى هذه الغرفة."</string>
<string name="screen_media_browser_media_empty_state_title">"لم يتم رفع أي وسائط بعد"</string>
<string name="screen_media_browser_title">"الوسائط والملفات"</string>
<string name="screen_media_details_file_format">"التنسيق"</string>
<string name="screen_media_details_filename">"الاسم"</string>
<string name="screen_media_details_no_more_files_to_show">"لا توجد ملفات أخرى لعرضها"</string>
<string name="screen_media_details_no_more_media_to_show">"لا توجد وسائط أخرى لعرضها"</string>
<string name="screen_media_details_title">"معلومات الملف"</string>
<string name="screen_media_details_uploaded_by">"تم الرفع بواسطة"</string>
<string name="screen_media_details_uploaded_on">"تم الرفع في"</string>
</resources>
@@ -6,16 +6,17 @@
<string name="screen_media_browser_files_empty_state_subtitle">"Los documentos, archivos de audio y mensajes de voz subidos a esta sala se mostrarán aquí."</string>
<string name="screen_media_browser_files_empty_state_title">"Aún no se ha subido ningún archivo"</string>
<string name="screen_media_browser_list_loading_files">"Cargando archivos…"</string>
<string name="screen_media_browser_list_loading_media">"Cargando medios…"</string>
<string name="screen_media_browser_list_loading_media">"Cargando multimedia…"</string>
<string name="screen_media_browser_list_mode_files">"Archivos"</string>
<string name="screen_media_browser_list_mode_media">"Medios"</string>
<string name="screen_media_browser_list_mode_media">"Multimedia"</string>
<string name="screen_media_browser_media_empty_state_subtitle">"Las imágenes y vídeos subidos a esta sala se mostrarán aquí."</string>
<string name="screen_media_browser_media_empty_state_title">"Aún no se ha subido ningún medio"</string>
<string name="screen_media_browser_title">"Medios y archivos"</string>
<string name="screen_media_browser_title">"Multimedia y archivos"</string>
<string name="screen_media_details_file_format">"Formato de archivo"</string>
<string name="screen_media_details_filename">"Nombre del archivo"</string>
<string name="screen_media_details_no_more_files_to_show">"No hay más archivos que mostrar"</string>
<string name="screen_media_details_no_more_media_to_show">"No hay más medios que mostrar"</string>
<string name="screen_media_details_title">"Información del archivo"</string>
<string name="screen_media_details_uploaded_by">"Subido por"</string>
<string name="screen_media_details_uploaded_on">"Subido el"</string>
</resources>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_media_browser_delete_confirmation_subtitle">"این فایل از اتاق حذف خواهد شد و اعضا به آن دسترسی نخواهند داشت."</string>
<string name="screen_media_browser_delete_confirmation_title">"حذف پرونده؟"</string>
<string name="screen_media_browser_download_error_message">"بررسی اتّصال اینترنتیتان و تلاش دوباره."</string>
<string name="screen_media_browser_files_empty_state_subtitle">"سندها، پرونده‌ها و پیام‌های صوتی بار گذاشته در این اتاق این‌جا نشان داده خواهند شد."</string>
@@ -13,6 +14,9 @@
<string name="screen_media_browser_title">"رسانه‌ها و پرونده‌ها"</string>
<string name="screen_media_details_file_format">"قالب پرونده"</string>
<string name="screen_media_details_filename">"نام پرونده"</string>
<string name="screen_media_details_no_more_files_to_show">"فایل بیشتری برای نمایش وجود ندارد"</string>
<string name="screen_media_details_no_more_media_to_show">"رسانه دیگری برای نمایش وجود ندارد"</string>
<string name="screen_media_details_title">"اطلاعات فایل"</string>
<string name="screen_media_details_uploaded_by">"بارگذاشته به دست"</string>
<string name="screen_media_details_uploaded_on">"بارگذاشته در"</string>
</resources>
@@ -16,6 +16,7 @@
<string name="screen_media_details_filename">"檔案名稱"</string>
<string name="screen_media_details_no_more_files_to_show">"無可顯示的檔案"</string>
<string name="screen_media_details_no_more_media_to_show">"無可顯示的媒體"</string>
<string name="screen_media_details_title">"檔案資訊"</string>
<string name="screen_media_details_uploaded_by">"上傳者:"</string>
<string name="screen_media_details_uploaded_on">"上傳於"</string>
</resources>
@@ -39,6 +39,7 @@ import io.element.android.libraries.matrix.ui.media.contentvalidation.NoopEventC
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
import io.element.android.libraries.mediaviewer.impl.model.MediaItem
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
@@ -171,6 +172,32 @@ class TimelineMediaGalleryDataSourceTest {
}
}
@Test
fun `test - failing to load a focused timeline keeps the item that was opened`() = runTest {
val room = FakeJoinedRoom(
createTimelineResult = { Result.failure(AN_EXCEPTION) },
roomCoroutineScope = backgroundScope,
)
val initialMediaItem = aMediaItemImage()
val sut = createTimelineMediaGalleryDataSource(
room = room,
mediaTimeline = FocusedMediaTimeline(
room = room,
eventId = AN_EVENT_ID,
onlyPinnedEvents = false,
initialMediaItem = initialMediaItem,
),
)
sut.start(backgroundScope)
sut.groupedMediaItemsFlow().test {
val item = awaitItem()
assertThat(item.isSuccess()).isTrue()
assertThat(item.dataOrNull()?.imageAndVideoItems).contains(initialMediaItem)
expectNoEvents()
}
assertThat(sut.getLastData().isFailure()).isFalse()
}
@Test
fun `test - when timeline emits new data, the flow emits the data`() = runTest {
val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList())
@@ -269,10 +296,11 @@ internal fun TestScope.createTimelineMediaGalleryDataSource(
room: JoinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(),
),
mediaTimeline: MediaTimeline? = null,
): TimelineMediaGalleryDataSource {
return TimelineMediaGalleryDataSource(
room = room,
mediaTimeline = LiveMediaTimeline(room),
mediaTimeline = mediaTimeline ?: LiveMediaTimeline(room),
timelineMediaItemsFactory = createTimelineMediaItemsFactory(),
mediaItemsPostProcessor = MediaItemsPostProcessor(),
)