Reimplement "Natural media viewer swiping order" (#6715)

This commit is contained in:
bxdxnn 2026-05-05 17:02:52 +03:00 committed by GitHub
parent 2d203e83b9
commit 28e1062eed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 33 additions and 56 deletions

View file

@ -122,25 +122,11 @@ class MediaViewerDataSource(
*/
private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = buildList {
// Filter out DateSeparator items, we do not need them for the media viewer
val itemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator }
// Separate loading indicators and media events
val loadingIndicators = itemsNoDateSeparator.filterIsInstance<MediaItem.LoadingIndicator>()
val mediaEvents = itemsNoDateSeparator.filterIsInstance<MediaItem.Event>()
// Determine backward and forward loading indicators
val backwardLoading = loadingIndicators.find { it.direction == Timeline.PaginationDirection.BACKWARDS }
val forwardLoading = loadingIndicators.find { it.direction == Timeline.PaginationDirection.FORWARDS }
// Build ordered list: backward loading, media events (oldest first), forward loading
// Media events are currently newest first, reverse to get oldest first
val orderedEvents = mediaEvents.reversed()
// Create new list of MediaItem in order: backwardLoading, orderedEvents, forwardLoading
val orderedItems = buildList {
backwardLoading?.let { add(it) }
addAll(orderedEvents)
forwardLoading?.let { add(it) }
}
pagerKeysHandler.accept(orderedItems)
orderedItems.forEach { mediaItem ->
val groupedItemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator }
pagerKeysHandler.accept(groupedItemsNoDateSeparator)
groupedItemsNoDateSeparator.forEach { mediaItem ->
when (mediaItem) {
is MediaItem.DateSeparator -> Unit
is MediaItem.Event -> {
val sourceUrl = mediaItem.mediaSource().safeUrl
val localMedia = localMediaStates.getOrPut(sourceUrl) {
@ -164,7 +150,6 @@ class MediaViewerDataSource(
pagerKey = pagerKeysHandler.getKey(mediaItem),
)
)
is MediaItem.DateSeparator -> Unit // already filtered out
}
}
}.toImmutableList()

View file

@ -177,21 +177,18 @@ class MediaViewerPresenter(
currentIndex: IntState,
data: State<ImmutableList<MediaViewerPageData>>,
) {
// With newest-first ordering, backward loading indicator is at the last index
val isRenderingLoadingBackward by remember {
derivedStateOf {
currentIndex.intValue == 0 &&
currentIndex.intValue == data.value.lastIndex &&
data.value.size > 1 &&
data.value.firstOrNull() is MediaViewerPageData.Loading &&
(data.value.firstOrNull() as? MediaViewerPageData.Loading)?.direction == Timeline.PaginationDirection.BACKWARDS
data.value.lastOrNull() is MediaViewerPageData.Loading
}
}
if (isRenderingLoadingBackward) {
LaunchedEffect(Unit) {
// Observe the loading data vanishing
snapshotFlow {
val first = data.value.firstOrNull()
first is MediaViewerPageData.Loading && first.direction == Timeline.PaginationDirection.BACKWARDS
}
snapshotFlow { data.value.lastOrNull() is MediaViewerPageData.Loading }
.distinctUntilChanged()
.filter { !it }
.onEach { showNoMoreItemsSnackbar() }
@ -205,21 +202,18 @@ class MediaViewerPresenter(
currentIndex: IntState,
data: State<ImmutableList<MediaViewerPageData>>,
) {
// With newest-first ordering, forward loading indicator is at the first index
val isRenderingLoadingForward by remember {
derivedStateOf {
currentIndex.intValue == data.value.lastIndex &&
currentIndex.intValue == 0 &&
data.value.size > 1 &&
data.value.lastOrNull() is MediaViewerPageData.Loading &&
(data.value.lastOrNull() as? MediaViewerPageData.Loading)?.direction == Timeline.PaginationDirection.FORWARDS
data.value.firstOrNull() is MediaViewerPageData.Loading
}
}
if (isRenderingLoadingForward) {
LaunchedEffect(Unit) {
// Observe the loading data vanishing
snapshotFlow {
val last = data.value.lastOrNull()
last is MediaViewerPageData.Loading && last.direction == Timeline.PaginationDirection.FORWARDS
}
snapshotFlow { data.value.firstOrNull() is MediaViewerPageData.Loading }
.distinctUntilChanged()
.filter { !it }
.onEach { showNoMoreItemsSnackbar() }

View file

@ -182,6 +182,7 @@ fun MediaViewerView(
// Pre-load previous and next pages
beyondViewportPageCount = 1,
key = { index -> state.listData[index].pagerKey },
reverseLayout = true,
) { page ->
when (val dataForPage = state.listData[page]) {
is MediaViewerPageData.Failure -> {

View file

@ -593,20 +593,20 @@ class MediaViewerPresenterTest {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
)
} else {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
fileItems = persistentListOf(),
)
}
)
)
val updatedState = awaitItem()
// User navigate to the last item (forward loading indicator)
// User navigate to the first item (forward loading indicator)
updatedState.eventSink(
MediaViewerEvent.OnNavigateTo(2)
MediaViewerEvent.OnNavigateTo(0)
)
// data source claims that there is no more items to load forward
mediaGalleryDataSource.emitGroupedMediaItems(
@ -614,21 +614,19 @@ class MediaViewerPresenterTest {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(aBackwardLoadingIndicator, anImage),
fileItems = persistentListOf(anImage, aBackwardLoadingIndicator),
)
} else {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage),
imageAndVideoItems = persistentListOf(anImage, aBackwardLoadingIndicator),
fileItems = persistentListOf(),
)
}
)
)
var stateWithSnackbar = awaitItem()
while (stateWithSnackbar.snackbarMessage == null) {
stateWithSnackbar = awaitItem()
}
assertThat(stateWithSnackbar.snackbarMessage.messageResId).isEqualTo(expectedSnackbarResId)
skipItems(1)
val stateWithSnackbar = awaitItem()
assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId)
}
}
@ -667,42 +665,41 @@ class MediaViewerPresenterTest {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
)
} else {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
fileItems = persistentListOf(),
)
}
)
)
val updatedState = awaitItem()
// User navigate to the first item (backward loading indicator)
// User navigate to the last item (backward loading indicator)
updatedState.eventSink(
MediaViewerEvent.OnNavigateTo(0)
MediaViewerEvent.OnNavigateTo(2)
)
skipItems(1)
// data source claims that there is no more items to load backward
mediaGalleryDataSource.emitGroupedMediaItems(
AsyncData.Success(
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(anImage, aForwardLoadingIndicator),
fileItems = persistentListOf(aForwardLoadingIndicator, anImage),
)
} else {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(anImage, aForwardLoadingIndicator),
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage),
fileItems = persistentListOf(),
)
}
)
)
var stateWithSnackbar = awaitItem()
while (stateWithSnackbar.snackbarMessage == null) {
stateWithSnackbar = awaitItem()
}
assertThat(stateWithSnackbar.snackbarMessage.messageResId).isEqualTo(expectedSnackbarResId)
skipItems(1)
val stateWithSnackbar = awaitItem()
assertThat(stateWithSnackbar.snackbarMessage!!.messageResId).isEqualTo(expectedSnackbarResId)
}
}
@ -720,7 +717,7 @@ class MediaViewerPresenterTest {
mediaGalleryDataSource.emitGroupedMediaItems(
AsyncData.Success(
GroupedMediaItems(
imageAndVideoItems = persistentListOf(aBackwardLoadingIndicator, anImage, aForwardLoadingIndicator),
imageAndVideoItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
fileItems = persistentListOf(),
)
)