Only load full media on media viewer when it's the visible item (#6794)
* Only load full media on media viewer when it's the visible item * Allow cancelling ongoing media loading if scrolling fast
This commit is contained in:
parent
57022926c6
commit
2a8ba7fa95
5 changed files with 49 additions and 7 deletions
|
|
@ -19,6 +19,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
||||||
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
|
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.media.MediaFile
|
||||||
|
import io.element.android.libraries.matrix.api.media.MediaSource
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode
|
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint.MediaViewerMode
|
||||||
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
|
||||||
|
|
@ -40,6 +41,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class MediaViewerDataSource(
|
class MediaViewerDataSource(
|
||||||
mode: MediaViewerMode,
|
mode: MediaViewerMode,
|
||||||
|
|
@ -51,7 +53,7 @@ class MediaViewerDataSource(
|
||||||
private val pagerKeysHandler: PagerKeysHandler,
|
private val pagerKeysHandler: PagerKeysHandler,
|
||||||
) {
|
) {
|
||||||
// List of media files that are currently being loaded
|
// List of media files that are currently being loaded
|
||||||
private val mediaFiles: MutableList<MediaFile> = mutableListOf()
|
private val mediaFiles: ConcurrentHashMap<MediaSource, MediaFile> = ConcurrentHashMap()
|
||||||
|
|
||||||
private val galleryMode = when (mode) {
|
private val galleryMode = when (mode) {
|
||||||
MediaViewerMode.SingleMedia,
|
MediaViewerMode.SingleMedia,
|
||||||
|
|
@ -69,7 +71,7 @@ class MediaViewerDataSource(
|
||||||
|
|
||||||
fun dispose() {
|
fun dispose() {
|
||||||
Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files")
|
Timber.d("Disposing MediaViewerDataSource, closing ${mediaFiles.size} media files")
|
||||||
mediaFiles.forEach { it.close() }
|
mediaFiles.values.forEach { it.close() }
|
||||||
mediaFiles.clear()
|
mediaFiles.clear()
|
||||||
localMediaStates.clear()
|
localMediaStates.clear()
|
||||||
}
|
}
|
||||||
|
|
@ -163,6 +165,12 @@ class MediaViewerDataSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) {
|
suspend fun loadMedia(data: MediaViewerPageData.MediaViewerData) {
|
||||||
|
val currentState = localMediaStates[data.mediaSource.safeUrl]?.value
|
||||||
|
// If the media is already loading or has been loaded successfully, do nothing
|
||||||
|
if (currentState?.isLoading() == true || currentState?.isSuccess() == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Timber.d("loadMedia for ${data.eventId}")
|
Timber.d("loadMedia for ${data.eventId}")
|
||||||
val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) {
|
val localMediaState = localMediaStates.getOrPut(data.mediaSource.safeUrl) {
|
||||||
mutableStateOf(AsyncData.Uninitialized)
|
mutableStateOf(AsyncData.Uninitialized)
|
||||||
|
|
@ -175,7 +183,7 @@ class MediaViewerDataSource(
|
||||||
filename = data.mediaInfo.filename
|
filename = data.mediaInfo.filename
|
||||||
)
|
)
|
||||||
.onSuccess { mediaFile ->
|
.onSuccess { mediaFile ->
|
||||||
mediaFiles.add(mediaFile)
|
mediaFiles[data.mediaSource] = mediaFile
|
||||||
}
|
}
|
||||||
.mapCatchingExceptions { mediaFile ->
|
.mapCatchingExceptions { mediaFile ->
|
||||||
localMediaFactory.createFromMediaFile(
|
localMediaFactory.createFromMediaFile(
|
||||||
|
|
@ -190,4 +198,12 @@ class MediaViewerDataSource(
|
||||||
localMediaState.value = AsyncData.Failure(it)
|
localMediaState.value = AsyncData.Failure(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancelLoadingMedia(data: MediaViewerPageData.MediaViewerData) {
|
||||||
|
if (localMediaStates[data.mediaSource.safeUrl]?.value?.isLoading() == true) {
|
||||||
|
Timber.d("cancelLoadingMedia for ${data.eventId}")
|
||||||
|
mediaFiles.remove(data.mediaSource)?.close()
|
||||||
|
localMediaStates[data.mediaSource.safeUrl]?.value = AsyncData.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,5 @@ sealed interface MediaViewerEvent {
|
||||||
data class Delete(val eventId: EventId) : MediaViewerEvent
|
data class Delete(val eventId: EventId) : MediaViewerEvent
|
||||||
data class OnNavigateTo(val index: Int) : MediaViewerEvent
|
data class OnNavigateTo(val index: Int) : MediaViewerEvent
|
||||||
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent
|
data class LoadMore(val direction: Timeline.PaginationDirection) : MediaViewerEvent
|
||||||
|
data class CancelLoadingMedia(val data: MediaViewerPageData.MediaViewerData) : MediaViewerEvent
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,9 @@ class MediaViewerPresenter(
|
||||||
is MediaViewerEvent.LoadMedia -> {
|
is MediaViewerEvent.LoadMedia -> {
|
||||||
coroutineScope.downloadMedia(data = event.data)
|
coroutineScope.downloadMedia(data = event.data)
|
||||||
}
|
}
|
||||||
|
is MediaViewerEvent.CancelLoadingMedia -> {
|
||||||
|
dataSource.cancelLoadingMedia(event.data)
|
||||||
|
}
|
||||||
is MediaViewerEvent.ClearLoadingError -> {
|
is MediaViewerEvent.ClearLoadingError -> {
|
||||||
dataSource.clearLoadingError(event.data)
|
dataSource.clearLoadingError(event.data)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.layout.onVisibilityChanged
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalInspectionMode
|
import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -208,11 +209,16 @@ fun MediaViewerView(
|
||||||
}
|
}
|
||||||
is MediaViewerPageData.MediaViewerData -> {
|
is MediaViewerPageData.MediaViewerData -> {
|
||||||
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
var bottomPaddingInPixels by remember { mutableIntStateOf(defaultBottomPaddingInPixels) }
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
|
|
||||||
}
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier
|
||||||
|
.onVisibilityChanged(minDurationMs = 200L) { isVisible ->
|
||||||
|
if (isVisible) {
|
||||||
|
state.eventSink(MediaViewerEvent.LoadMedia(dataForPage))
|
||||||
|
} else {
|
||||||
|
state.eventSink(MediaViewerEvent.CancelLoadingMedia(dataForPage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
val isDisplayed = remember(pagerState.settledPage) {
|
val isDisplayed = remember(pagerState.settledPage) {
|
||||||
// This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value
|
// This 'item provider' lambda will be called when the data source changes with an outdated `settlePage` value
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ class MediaViewerViewTest {
|
||||||
state = state,
|
state = state,
|
||||||
onBackClick = callback,
|
onBackClick = callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
|
mainClock.advanceTimeBy(200)
|
||||||
|
|
||||||
pressBack()
|
pressBack()
|
||||||
}
|
}
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
|
|
@ -110,6 +114,10 @@ class MediaViewerViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
|
mainClock.advanceTimeBy(200)
|
||||||
|
|
||||||
val contentDescription = activity!!.getString(contentDescriptionRes)
|
val contentDescription = activity!!.getString(contentDescriptionRes)
|
||||||
onNodeWithContentDescription(contentDescription).performClick()
|
onNodeWithContentDescription(contentDescription).performClick()
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
|
|
@ -241,6 +249,10 @@ class MediaViewerViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
|
mainClock.advanceTimeBy(200)
|
||||||
|
|
||||||
clickOn(CommonStrings.action_retry)
|
clickOn(CommonStrings.action_retry)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -263,6 +275,10 @@ class MediaViewerViewTest {
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for enough time for the onVisibilityChanged modifier to trigger
|
||||||
|
mainClock.advanceTimeBy(200)
|
||||||
|
|
||||||
clickOn(CommonStrings.action_cancel)
|
clickOn(CommonStrings.action_cancel)
|
||||||
eventsRecorder.assertList(
|
eventsRecorder.assertList(
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue