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 new file mode 100644 index 0000000000..2b86ee841b --- /dev/null +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerDataSource.kt @@ -0,0 +1,151 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.mediaviewer.impl.viewer + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaFile +import io.element.android.libraries.matrix.api.timeline.Timeline +import io.element.android.libraries.mediaviewer.api.local.LocalMedia +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource +import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode +import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem +import io.element.android.libraries.mediaviewer.impl.gallery.eventId +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.toPersistentList +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext +import timber.log.Timber + +class MediaViewerDataSource( + private val galleryMode: MediaGalleryMode, + private val dispatcher: CoroutineDispatcher, + private val galleryDataSource: MediaGalleryDataSource, + private val mediaLoader: MatrixMediaLoader, + private val localMediaFactory: LocalMediaFactory, +) { + + // List of media files that are currently being loaded + private val mediaFiles: MutableList = mutableListOf() + + // Map of sourceUrl to local media state + private val localMediaStates: MutableMap>> = + mutableMapOf() + + fun setup() { + galleryDataSource.start() + } + + fun dispose() { + mediaFiles.forEach { it.close() } + mediaFiles.clear() + localMediaStates.clear() + } + + fun initialPageIndex(eventId: EventId?): Int { + if (eventId == null) { + return 0 + } + val mediaItems = + galleryDataSource.getLastData().dataOrNull()?.getItems(galleryMode).orEmpty() + val pageList = buildMediaViewerPageList(mediaItems) + return pageList.indexOfFirst { data -> + when (data) { + is MediaViewerPageData.MediaViewerData -> data.eventId == eventId + else -> false + } + } + .takeIf { it != -1 } + ?: 0 + } + + fun dataFlow(): Flow> { + return galleryDataSource.groupedMediaItemsFlow() + .map { + val groupedItems = it.dataOrNull()?.getItems(galleryMode).orEmpty() + withContext(dispatcher) { + buildMediaViewerPageList(groupedItems) + } + } + } + + private fun buildMediaViewerPageList(groupedItems: List) = buildList { + groupedItems.forEach { mediaItem -> + when (mediaItem) { + is MediaItem.DateSeparator -> Unit + is MediaItem.Event -> { + val sourceUrl = mediaItem.mediaSource().url + val localMedia = localMediaStates.getOrPut(sourceUrl) { + mutableStateOf(AsyncData.Uninitialized) + } + add( + MediaViewerPageData.MediaViewerData( + eventId = mediaItem.eventId(), + mediaInfo = mediaItem.mediaInfo(), + mediaSource = mediaItem.mediaSource(), + thumbnailSource = mediaItem.thumbnailSource(), + downloadedMedia = localMedia, + ) + ) + } + is MediaItem.LoadingIndicator -> add( + MediaViewerPageData.Loading(mediaItem.direction) + ) + } + } + if (isEmpty()) { + MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS) + } + }.toPersistentList() + + suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) { + Timber.d("loadMedia for ${data.eventId}") + val localMediaState = localMediaStates.getOrPut(data.mediaSource.url) { + mutableStateOf(AsyncData.Uninitialized) + } + localMediaState.value = AsyncData.Loading() + mediaLoader + .downloadMediaFile( + source = data.mediaSource, + mimeType = data.mediaInfo.mimeType, + filename = data.mediaInfo.filename + ) + .onSuccess { mediaFile -> + mediaFiles.add(mediaFile) + } + .mapCatching { mediaFile -> + localMediaFactory.createFromMediaFile( + mediaFile = mediaFile, + mediaInfo = data.mediaInfo + ) + } + .onSuccess { + localMediaState.value = AsyncData.Success(it) + } + .onFailure { + localMediaState.value = AsyncData.Failure(it) + } + } + + fun clearLoadingError(data: MediaViewerPageData.MediaViewerData) { + localMediaStates[data.mediaSource.url]?.value = AsyncData.Uninitialized + } + + suspend fun loadMore(direction: Timeline.PaginationDirection) { + galleryDataSource.loadMore(direction) + } +} diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt index 674e481b9e..708c423d36 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerEvents.kt @@ -15,7 +15,7 @@ sealed interface MediaViewerEvents { data class SaveOnDisk(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class Share(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class OpenWith(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents - data class ClearLoadingError(val eventId: EventId) : MediaViewerEvents + data class ClearLoadingError(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class ViewInTimeline(val eventId: EventId) : MediaViewerEvents data class OpenInfo(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvents data class ConfirmDelete( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt index 359347fb31..05a5fb759c 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerNode.kt @@ -18,9 +18,13 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint +import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory +import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode import io.element.android.libraries.mediaviewer.impl.gallery.SingleMediaGalleryDataSource import io.element.android.libraries.mediaviewer.impl.gallery.TimelineMediaGalleryDataSource @@ -30,6 +34,9 @@ class MediaViewerNode @AssistedInject constructor( @Assisted plugins: List, presenterFactory: MediaViewerPresenter.Factory, timelineMediaGalleryDataSource: TimelineMediaGalleryDataSource, + mediaLoader: MatrixMediaLoader, + localMediaFactory: LocalMediaFactory, + coroutineDispatchers: CoroutineDispatchers, ) : Node(buildContext, plugins = plugins), MediaViewerNavigator { private val inputs = inputs() @@ -50,14 +57,28 @@ class MediaViewerNode @AssistedInject constructor( onDone() } + private val mediaGallerySource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) { + SingleMediaGalleryDataSource.createFrom(inputs) + } else { + timelineMediaGalleryDataSource + } + + private val galleryMode = when (inputs.mode) { + MediaViewerEntryPoint.MediaViewerMode.SingleMedia, + MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images + MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files + } + private val presenter = presenterFactory.create( inputs = inputs, navigator = this, - mediaGalleryDataSource = if (inputs.mode == MediaViewerEntryPoint.MediaViewerMode.SingleMedia) { - SingleMediaGalleryDataSource.createFrom(inputs) - } else { - timelineMediaGalleryDataSource - }, + dataSource = MediaViewerDataSource( + dispatcher = coroutineDispatchers.computation, + galleryMode = galleryMode, + galleryDataSource = mediaGallerySource, + mediaLoader = mediaLoader, + localMediaFactory = localMediaFactory + ) ) @Composable diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt index c5e29aca12..176288b537 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/viewer/MediaViewerPresenter.kt @@ -10,10 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer import android.content.ActivityNotFoundException import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -29,26 +26,17 @@ import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatch import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.designsystem.utils.snackbar.collectSnackbarMessageAsState import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.media.MatrixMediaLoader -import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn -import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint import io.element.android.libraries.mediaviewer.api.local.LocalMedia -import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryDataSource -import io.element.android.libraries.mediaviewer.impl.gallery.MediaGalleryMode -import io.element.android.libraries.mediaviewer.impl.gallery.MediaItem -import io.element.android.libraries.mediaviewer.impl.gallery.eventId -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 io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import io.element.android.libraries.androidutils.R as UtilsR @@ -56,10 +44,8 @@ import io.element.android.libraries.androidutils.R as UtilsR class MediaViewerPresenter @AssistedInject constructor( @Assisted private val inputs: MediaViewerEntryPoint.Params, @Assisted private val navigator: MediaViewerNavigator, - @Assisted private val mediaGalleryDataSource: MediaGalleryDataSource, + @Assisted private val dataSource: MediaViewerDataSource, private val room: MatrixRoom, - private val localMediaFactory: LocalMediaFactory, - private val mediaLoader: MatrixMediaLoader, private val localMediaActions: LocalMediaActions, private val snackbarDispatcher: SnackbarDispatcher, ) : Presenter { @@ -68,97 +54,46 @@ class MediaViewerPresenter @AssistedInject constructor( fun create( inputs: MediaViewerEntryPoint.Params, navigator: MediaViewerNavigator, - mediaGalleryDataSource: MediaGalleryDataSource, + dataSource: MediaViewerDataSource, ): MediaViewerPresenter } - private val galleryMode = when (inputs.mode) { - MediaViewerEntryPoint.MediaViewerMode.SingleMedia, - MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images - MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files - } - @Composable override fun present(): MediaViewerState { val coroutineScope = rememberCoroutineScope() - LaunchedEffect(Unit) { - mediaGalleryDataSource.start() - } - val groupedMediaItem by remember { mediaGalleryDataSource.groupedMediaItemsFlow() } - .collectAsState(mediaGalleryDataSource.getLastData()) - - val mediaFile: MutableMap> = remember { mutableMapOf() } - val localMedia: MutableMap>> = remember { mutableMapOf() } - DisposableEffect(Unit) { - onDispose { - mediaFile.values.forEach { it.value?.close() } - } - } - - val data: List by remember { - derivedStateOf { - buildList { - val data = groupedMediaItem.dataOrNull() - if (data != null) { - if (data.getItems(galleryMode).firstOrNull() is MediaItem.LoadingIndicator) { - add(MediaViewerPageData.Loading(Timeline.PaginationDirection.FORWARDS)) - } - data.getItems(galleryMode).filterIsInstance().forEach { mediaItem -> - val eventId = mediaItem.eventId() - add( - MediaViewerPageData.MediaViewerData( - eventId = eventId, - mediaInfo = mediaItem.mediaInfo(), - mediaSource = mediaItem.mediaSource(), - thumbnailSource = mediaItem.thumbnailSource(), - downloadedMedia = localMedia.getOrPut(eventId) { - mutableStateOf(AsyncData.Uninitialized) - }.value, - ) - ) - } - if (data.getItems(galleryMode).lastOrNull() is MediaItem.LoadingIndicator) { - add(MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS)) - } - } - if (isEmpty()) { - add(MediaViewerPageData.Loading(Timeline.PaginationDirection.BACKWARDS)) - } - } - } - } - var currentIndex by remember { mutableIntStateOf(searchIndex(data, inputs.eventId)) } + val data: ImmutableList by dataSource.dataFlow().collectAsState(persistentListOf()) + var currentIndex by remember { mutableIntStateOf(dataSource.initialPageIndex(inputs.eventId)) } val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() - localMediaActions.Configure() + var mediaBottomSheetState by remember { mutableStateOf(MediaBottomSheetState.Hidden) } + DisposableEffect(Unit) { + dataSource.setup() + onDispose { + dataSource.dispose() + } + } + localMediaActions.Configure() + fun handleEvents(event: MediaViewerEvents) { when (event) { is MediaViewerEvents.LoadMedia -> { - // It's OK to suppress the warning since mediaFile and localMedia are remembered - @Suppress("RememberMissing") - coroutineScope.downloadMedia( - data = event.data, - mediaFile = mediaFile.getOrPut(event.data.eventId) { mutableStateOf(null) }, - localMedia = localMedia.getOrPut(event.data.eventId) { mutableStateOf(AsyncData.Uninitialized) }, - ) + coroutineScope.downloadMedia(data = event.data) } is MediaViewerEvents.ClearLoadingError -> { - // It's OK to suppress the warning since localMedia is remembered - @Suppress("RememberMissing") - localMedia.getOrPut(event.eventId) { mutableStateOf(AsyncData.Uninitialized) }.value = AsyncData.Uninitialized + dataSource.clearLoadingError(event.data) } is MediaViewerEvents.SaveOnDisk -> { mediaBottomSheetState = MediaBottomSheetState.Hidden - coroutineScope.saveOnDisk(event.data.downloadedMedia) + coroutineScope.saveOnDisk(event.data.downloadedMedia.value) } is MediaViewerEvents.Share -> { mediaBottomSheetState = MediaBottomSheetState.Hidden - coroutineScope.share(event.data.downloadedMedia) + coroutineScope.share(event.data.downloadedMedia.value) } is MediaViewerEvents.OpenWith -> { mediaBottomSheetState = MediaBottomSheetState.Hidden - coroutineScope.open(event.data.downloadedMedia) + coroutineScope.open(event.data.downloadedMedia.value) } is MediaViewerEvents.Delete -> { mediaBottomSheetState = MediaBottomSheetState.Hidden @@ -194,7 +129,7 @@ class MediaViewerPresenter @AssistedInject constructor( currentIndex = event.index } is MediaViewerEvents.LoadMore -> coroutineScope.launch { - mediaGalleryDataSource.loadMore(event.direction) + dataSource.loadMore(event.direction) } } } @@ -211,30 +146,8 @@ class MediaViewerPresenter @AssistedInject constructor( private fun CoroutineScope.downloadMedia( data: MediaViewerPageData.MediaViewerData, - mediaFile: MutableState, - localMedia: MutableState>, ) = launch { - localMedia.value = AsyncData.Loading() - mediaLoader.downloadMediaFile( - source = data.mediaSource, - mimeType = data.mediaInfo.mimeType, - filename = data.mediaInfo.filename - ) - .onSuccess { - mediaFile.value = it - } - .mapCatching { mediaFile -> - localMediaFactory.createFromMediaFile( - mediaFile = mediaFile, - mediaInfo = data.mediaInfo - ) - } - .onSuccess { - localMedia.value = AsyncData.Success(it) - } - .onFailure { - localMedia.value = AsyncData.Failure(it) - } + dataSource.loadMedia(data) } private fun CoroutineScope.saveOnDisk(localMedia: AsyncData) = launch { @@ -290,14 +203,3 @@ class MediaViewerPresenter @AssistedInject constructor( } } } - -private fun searchIndex(data: List, eventId: EventId?): Int { - if (eventId == null) { - return 0 - } - return data.indexOfFirst { - (it as? MediaViewerPageData.MediaViewerData)?.eventId == eventId - } - .takeIf { it != -1 } - ?: 0 -} 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 7ad1bc9982..e2b8170a78 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 @@ -7,6 +7,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer +import androidx.compose.runtime.State import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage import io.element.android.libraries.matrix.api.core.EventId @@ -15,9 +16,10 @@ import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.mediaviewer.api.MediaInfo import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState +import kotlinx.collections.immutable.ImmutableList data class MediaViewerState( - val listData: List, + val listData: ImmutableList, val currentIndex: Int, val snackbarMessage: SnackbarMessage?, val canShowInfo: Boolean, @@ -35,6 +37,6 @@ sealed interface MediaViewerPageData { val mediaInfo: MediaInfo, val mediaSource: MediaSource, val thumbnailSource: MediaSource?, - val downloadedMedia: AsyncData, + val downloadedMedia: State>, ) : 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 6ba6bb74ae..d37003ed60 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 @@ -8,6 +8,7 @@ package io.element.android.libraries.mediaviewer.impl.viewer import android.net.Uri +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.media.aWaveForm @@ -22,6 +23,7 @@ import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.details.aMediaDeleteConfirmationState import io.element.android.libraries.mediaviewer.impl.details.aMediaDetailsBottomSheetState +import kotlinx.collections.immutable.toPersistentList open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -158,7 +160,7 @@ fun aMediaViewerPageData( mediaInfo = mediaInfo, mediaSource = mediaSource, thumbnailSource = null, - downloadedMedia = downloadedMedia, + downloadedMedia = mutableStateOf(downloadedMedia), ) fun aMediaViewerState( @@ -168,7 +170,7 @@ fun aMediaViewerState( mediaBottomSheetState: MediaBottomSheetState = MediaBottomSheetState.Hidden, eventSink: (MediaViewerEvents) -> Unit = {}, ) = MediaViewerState( - listData = listData, + listData = listData.toPersistentList(), currentIndex = currentIndex, snackbarMessage = null, canShowInfo = canShowInfo, 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 a30f78dfce..3c2b986c98 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 @@ -85,6 +85,7 @@ import me.saket.telephoto.flick.FlickToDismissState import me.saket.telephoto.flick.rememberFlickToDismissState import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.rememberZoomableState +import timber.log.Timber import kotlin.time.Duration @Composable @@ -98,7 +99,7 @@ fun MediaViewerView( val defaultBottomPaddingInPixels = if (LocalInspectionMode.current) 303 else 0 var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) } - val currentData = state.listData[state.currentIndex] + val currentData = state.listData.getOrNull(state.currentIndex) BackHandler { onBackClick() } Scaffold( modifier, @@ -113,6 +114,9 @@ fun MediaViewerView( state.eventSink(MediaViewerEvents.OnNavigateTo(page)) } } + LaunchedEffect(state.listData) { + Timber.d("MediaViewerView: state.listData: ${state.listData}") + } HorizontalPager( state = pagerState, modifier = Modifier, @@ -139,10 +143,15 @@ fun MediaViewerView( showOverlay = showOverlay, bottomPaddingInPixels = bottomPaddingInPixels, data = dataForPage, - state = state, onDismiss = { onBackClick() }, + onRetry = { + state.eventSink(MediaViewerEvents.LoadMedia(dataForPage)) + }, + onDismissError = { + state.eventSink(MediaViewerEvents.ClearLoadingError(dataForPage)) + }, onShowOverlayChange = { showOverlay = it } @@ -151,8 +160,8 @@ fun MediaViewerView( AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { Box( modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + .fillMaxSize() + .navigationBarsPadding() ) { MediaViewerBottomBar( modifier = Modifier.align(Alignment.BottomCenter), @@ -169,19 +178,10 @@ fun MediaViewerView( AnimatedVisibility(visible = showOverlay, enter = fadeIn(), exit = fadeOut()) { Box( modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + .fillMaxSize() + .navigationBarsPadding() ) { when (currentData) { - is MediaViewerPageData.Loading -> { - TopAppBar( - title = {}, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent.copy(0.6f), - ), - navigationIcon = { BackButton(onClick = onBackClick) }, - ) - } is MediaViewerPageData.MediaViewerData -> { MediaViewerTopBar( data = currentData, @@ -193,6 +193,15 @@ fun MediaViewerView( eventSink = state.eventSink ) } + else -> { + TopAppBar( + title = {}, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent.copy(0.6f), + ), + navigationIcon = { BackButton(onClick = onBackClick) }, + ) + } } } } @@ -250,21 +259,13 @@ private fun MediaViewerPage( isDisplayed: Boolean, showOverlay: Boolean, bottomPaddingInPixels: Int, - state: MediaViewerState, data: MediaViewerPageData.MediaViewerData, onDismiss: () -> Unit, + onRetry: () -> Unit, + onDismissError: () -> Unit, onShowOverlayChange: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { - fun onRetry() { - state.eventSink(MediaViewerEvents.LoadMedia(data)) - } - - fun onDismissError() { - data.eventId?.let { - state.eventSink(MediaViewerEvents.ClearLoadingError(it)) - } - } val currentShowOverlay by rememberUpdatedState(showOverlay) val currentOnShowOverlayChange by rememberUpdatedState(onShowOverlayChange) @@ -285,12 +286,13 @@ private fun MediaViewerPage( state = flickState, modifier = modifier.background(backgroundColorFor(flickState)) ) { - val showProgress = rememberShowProgress(data.downloadedMedia) + val downloadedMedia by data.downloadedMedia + val showProgress = rememberShowProgress(downloadedMedia) Box( modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() + .fillMaxSize() + .navigationBarsPadding() ) { Box(contentAlignment = Alignment.Center) { val zoomableState = rememberZoomableState( @@ -299,7 +301,7 @@ private fun MediaViewerPage( val localMediaViewState = rememberLocalMediaViewState(zoomableState) val showThumbnail = !localMediaViewState.isReady val playableState = localMediaViewState.playableState - val showError = data.downloadedMedia is AsyncData.Failure + val showError = downloadedMedia.isFailure() LaunchedEffect(playableState) { if (playableState is PlayableState.Playable) { @@ -312,7 +314,7 @@ private fun MediaViewerPage( isDisplayed = isDisplayed, bottomPaddingInPixels = bottomPaddingInPixels, localMediaViewState = localMediaViewState, - localMedia = data.downloadedMedia.dataOrNull(), + localMedia = downloadedMedia.dataOrNull(), mediaInfo = data.mediaInfo, onClick = { if (playableState is PlayableState.NotPlayable) { @@ -328,16 +330,16 @@ private fun MediaViewerPage( if (showError) { ErrorView( errorMessage = stringResource(id = CommonStrings.error_unknown), - onRetry = ::onRetry, - onDismiss = ::onDismissError + onRetry = onRetry, + onDismiss = onDismissError ) } } if (showProgress) { LinearProgressIndicator( modifier = Modifier - .fillMaxWidth() - .height(2.dp) + .fillMaxWidth() + .height(2.dp) ) } } @@ -366,8 +368,8 @@ private fun MediaViewerLoadingPage( ) { Box( modifier = Modifier - .fillMaxSize() - .navigationBarsPadding(), + .fillMaxSize() + .navigationBarsPadding(), contentAlignment = Alignment.Center ) { AsyncLoading() @@ -429,7 +431,8 @@ private fun MediaViewerTopBar( onInfoClick: () -> Unit, eventSink: (MediaViewerEvents) -> Unit, ) { - val actionsEnabled = data.downloadedMedia is AsyncData.Success + val downloadedMedia by data.downloadedMedia + val actionsEnabled = downloadedMedia.isSuccess() val mimeType = data.mediaInfo.mimeType val senderName = data.mediaInfo.senderName val dateSent = data.mediaInfo.dateSent @@ -503,11 +506,11 @@ private fun MediaViewerBottomBar( ) { Column( modifier = modifier - .fillMaxWidth() - .background(Color(0x99101317)) - .onSizeChanged { - onHeightChange(it.height) - }, + .fillMaxWidth() + .background(Color(0x99101317)) + .onSizeChanged { + onHeightChange(it.height) + }, ) { if (caption != null) { if (showDivider) { @@ -515,8 +518,8 @@ private fun MediaViewerBottomBar( } Text( modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + .fillMaxWidth() + .padding(16.dp), text = caption, maxLines = 5, overflow = TextOverflow.Ellipsis,