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 6abb83547b
commit 8e8426b08c
4 changed files with 33 additions and 56 deletions

View file

@ -122,25 +122,11 @@ class MediaViewerDataSource(
*/ */
private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = buildList { private fun buildMediaViewerPageList(groupedItems: List<MediaItem>) = buildList {
// Filter out DateSeparator items, we do not need them for the media viewer // Filter out DateSeparator items, we do not need them for the media viewer
val itemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator } val groupedItemsNoDateSeparator = groupedItems.filterNot { it is MediaItem.DateSeparator }
// Separate loading indicators and media events pagerKeysHandler.accept(groupedItemsNoDateSeparator)
val loadingIndicators = itemsNoDateSeparator.filterIsInstance<MediaItem.LoadingIndicator>() groupedItemsNoDateSeparator.forEach { mediaItem ->
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 ->
when (mediaItem) { when (mediaItem) {
is MediaItem.DateSeparator -> Unit
is MediaItem.Event -> { is MediaItem.Event -> {
val sourceUrl = mediaItem.mediaSource().safeUrl val sourceUrl = mediaItem.mediaSource().safeUrl
val localMedia = localMediaStates.getOrPut(sourceUrl) { val localMedia = localMediaStates.getOrPut(sourceUrl) {
@ -164,7 +150,6 @@ class MediaViewerDataSource(
pagerKey = pagerKeysHandler.getKey(mediaItem), pagerKey = pagerKeysHandler.getKey(mediaItem),
) )
) )
is MediaItem.DateSeparator -> Unit // already filtered out
} }
} }
}.toImmutableList() }.toImmutableList()

View file

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

View file

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

View file

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