From 2267a4b7872f14255697410ebbb6cf573aed6bec Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 22 Jan 2025 11:15:53 +0100 Subject: [PATCH] MediaViewer: add error case in the UI. --- .../impl/viewer/MediaViewerDataSource.kt | 13 ++++- .../impl/viewer/MediaViewerState.kt | 4 ++ .../impl/viewer/MediaViewerStateProvider.kt | 5 ++ .../impl/viewer/MediaViewerView.kt | 56 +++++++++++++++++-- .../impl/viewer/MediaViewerDataSourceTest.kt | 3 +- 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt index d03b003440..97d5f626b0 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -28,6 +28,7 @@ import io.element.android.libraries.mediaviewer.impl.gallery.mediaInfo import io.element.android.libraries.mediaviewer.impl.gallery.mediaSource import io.element.android.libraries.mediaviewer.impl.gallery.thumbnailSource import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -68,9 +69,15 @@ class MediaViewerDataSource( fun dataFlow(): Flow> { return galleryDataSource.groupedMediaItemsFlow() .map { groupedItems -> - val mediaItems = groupedItems.dataOrNull()?.getItems(galleryMode).orEmpty() - withContext(dispatcher) { - buildMediaViewerPageList(mediaItems) + if (groupedItems is AsyncData.Failure) { + persistentListOf( + MediaViewerPageData.Failure(groupedItems.error), + ) + } else { + val mediaItems = groupedItems.dataOrNull()?.getItems(galleryMode).orEmpty() + withContext(dispatcher) { + buildMediaViewerPageList(mediaItems) + } } } } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt index e2b8170a78..60c4889f60 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerState.kt @@ -28,6 +28,10 @@ data class MediaViewerState( ) sealed interface MediaViewerPageData { + data class Failure( + val throwable: Throwable, + ) : MediaViewerPageData + data class Loading( val direction: Timeline.PaginationDirection, ) : MediaViewerPageData diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt index 77f3698302..88931c90fc 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerStateProvider.kt @@ -154,6 +154,11 @@ open class MediaViewerStateProvider : PreviewParameterProvider MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS) ), ), + aMediaViewerState( + listOf( + MediaViewerPageData.Failure(Exception("error")) + ), + ), ) } diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt index b059805b01..5e9d527585 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerView.kt @@ -55,6 +55,7 @@ import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo +import io.element.android.libraries.designsystem.components.async.AsyncFailure 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.dialogs.RetryDialog @@ -123,6 +124,14 @@ fun MediaViewerView( beyondViewportPageCount = 1, ) { page -> when (val dataForPage = state.listData[page]) { + is MediaViewerPageData.Failure -> { + MediaViewerErrorPage( + throwable = dataForPage.throwable, + onDismiss = { + onBackClick() + }, + ) + } is MediaViewerPageData.Loading -> { LaunchedEffect(Unit) { state.eventSink(MediaViewerEvents.LoadMore(dataForPage.direction)) @@ -200,11 +209,13 @@ fun MediaViewerView( else -> { TopAppBar( title = { - Text( - text = stringResource(id = CommonStrings.common_loading_more), - style = ElementTheme.typography.fontBodyMdMedium, - color = ElementTheme.colors.textPrimary, - ) + if (currentData is MediaViewerPageData.Loading) { + Text( + text = stringResource(id = CommonStrings.common_loading_more), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textPrimary, + ) + } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = Color.Transparent.copy(0.6f), @@ -386,6 +397,41 @@ private fun MediaViewerLoadingPage( } } +@Composable +private fun MediaViewerErrorPage( + throwable: Throwable, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val flickState = rememberFlickToDismissState(dismissThresholdRatio = 0.1f, rotateOnDrag = false) + + DismissFlickEffects( + flickState = flickState, + onDismissing = { animationDuration -> + delay(animationDuration / 3) + onDismiss() + }, + onDragging = {}, + ) + + FlickToDismiss( + state = flickState, + modifier = modifier.background(backgroundColorFor(flickState)) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .navigationBarsPadding(), + contentAlignment = Alignment.Center + ) { + AsyncFailure( + throwable = throwable, + onRetry = null + ) + } + } +} + @Composable private fun DismissFlickEffects( flickState: FlickToDismissState, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt index 092467a63d..e7a684a44d 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSourceTest.kt @@ -69,8 +69,7 @@ class MediaViewerDataSourceTest { galleryDataSource.emitGroupedMediaItems(AsyncData.Loading()) assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) galleryDataSource.emitGroupedMediaItems(AsyncData.Failure(AN_EXCEPTION)) - // TODO Add an error screen in the ui - assertThat(awaitItem().first()).isInstanceOf(MediaViewerPageData.Loading::class.java) + assertThat(awaitItem().first()).isEqualTo(MediaViewerPageData.Failure(AN_EXCEPTION)) } }