Merge pull request #7362 from hayaksi1/fix/4973-media-gallery-error

Keep the opened image on screen when the media gallery fails to load
This commit is contained in:
Benoit Marty
2026-08-11 14:15:01 +02:00
committed by GitHub
5 changed files with 40 additions and 12 deletions
@@ -83,7 +83,9 @@ class TimelineMediaGalleryDataSource(
}, },
{ {
Timber.e(it, "Failed to get media timeline for room ${room.roomId}") 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 -> }.flatMapLatest { 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.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon 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.AsyncIndicator
import io.element.android.libraries.designsystem.components.async.AsyncLoading import io.element.android.libraries.designsystem.components.async.AsyncLoading
import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.button.BackButton
@@ -209,7 +208,6 @@ fun MediaViewerView(
when (val dataForPage = state.listData[page]) { when (val dataForPage = state.listData[page]) {
is MediaViewerPageData.Failure -> { is MediaViewerPageData.Failure -> {
MediaViewerErrorPage( MediaViewerErrorPage(
throwable = dataForPage.throwable,
onDismiss = onBackClick, onDismiss = onBackClick,
) )
} }
@@ -507,7 +505,6 @@ private fun MediaViewerLoadingPage(
@Composable @Composable
private fun MediaViewerErrorPage( private fun MediaViewerErrorPage(
throwable: Throwable,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@@ -521,9 +518,10 @@ private fun MediaViewerErrorPage(
.navigationBarsPadding(), .navigationBarsPadding(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
AsyncFailure( IconTitleSubtitleMolecule(
throwable = throwable, iconStyle = BigIcon.Style.AlertSolid,
onRetry = null title = stringResource(CommonStrings.common_something_went_wrong),
subTitle = stringResource(CommonStrings.common_something_went_wrong_message),
) )
} }
} }
@@ -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.api.MediaInfo
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems 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.MediaItem
import io.element.android.libraries.mediaviewer.impl.model.aMediaItemImage
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder 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 @Test
fun `test - when timeline emits new data, the flow emits the data`() = runTest { fun `test - when timeline emits new data, the flow emits the data`() = runTest {
val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList()) val timelineItems = MutableStateFlow<List<MatrixTimelineItem>>(emptyList())
@@ -269,10 +296,11 @@ internal fun TestScope.createTimelineMediaGalleryDataSource(
room: JoinedRoom = FakeJoinedRoom( room: JoinedRoom = FakeJoinedRoom(
liveTimeline = FakeTimeline(), liveTimeline = FakeTimeline(),
), ),
mediaTimeline: MediaTimeline? = null,
): TimelineMediaGalleryDataSource { ): TimelineMediaGalleryDataSource {
return TimelineMediaGalleryDataSource( return TimelineMediaGalleryDataSource(
room = room, room = room,
mediaTimeline = LiveMediaTimeline(room), mediaTimeline = mediaTimeline ?: LiveMediaTimeline(room),
timelineMediaItemsFactory = createTimelineMediaItemsFactory(), timelineMediaItemsFactory = createTimelineMediaItemsFactory(),
mediaItemsPostProcessor = MediaItemsPostProcessor(), mediaItemsPostProcessor = MediaItemsPostProcessor(),
) )