Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit df775739 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

chore: enforce fresh paging per submit to avoid stale results on repeat queries

parent 5b0e9cd6
Loading
Loading
Loading
Loading
+216 −84
Original line number Diff line number Diff line
@@ -33,6 +33,8 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
@@ -77,10 +79,32 @@ fun SearchResultsContent(
    onPrivacyClick: (Application) -> Unit = {},
    installButtonStateProvider: (Application) -> InstallButtonState,
) {
    if (tabs.isEmpty() || selectedTab !in tabs) {
    when {
        tabs.isEmpty() || selectedTab !in tabs -> {
            return
        }

        // Don't show tabs when a single source is checked in the Settings screen
        tabs.size == 1 -> {
            SearchTabPage(
                tab = selectedTab,
                fossItems = fossItems,
                pwaItems = pwaItems,
                playStoreItems = playStoreItems,
                searchVersion = searchVersion,
                getScrollPosition = getScrollPosition,
                onScrollPositionChange = onScrollPositionChange,
                onResultClick = onResultClick,
                onPrimaryActionClick = onPrimaryActionClick,
                onShowMoreClick = onShowMoreClick,
                onPrivacyClick = onPrivacyClick,
                installButtonStateProvider = installButtonStateProvider,
                modifier = modifier.fillMaxSize(),
            )
            return
        }

        else -> {
            val coroutineScope = rememberCoroutineScope()
            val selectedIndex = tabs.indexOf(selectedTab).coerceAtLeast(0)
            val pagerState = rememberPagerState(
@@ -126,17 +150,47 @@ fun SearchResultsContent(
                        .padding(top = 16.dp),
                ) { page ->
                    val tab = tabs[page]

            val items = when (tab) {
                SearchTabType.OPEN_SOURCE -> fossItems
                SearchTabType.PWA -> pwaItems
                else -> null
                    SearchTabPage(
                        tab = tab,
                        fossItems = fossItems,
                        pwaItems = pwaItems,
                        playStoreItems = playStoreItems,
                        searchVersion = searchVersion,
                        getScrollPosition = getScrollPosition,
                        onScrollPositionChange = onScrollPositionChange,
                        onResultClick = onResultClick,
                        onPrimaryActionClick = onPrimaryActionClick,
                        onShowMoreClick = onShowMoreClick,
                        onPrivacyClick = onPrivacyClick,
                        installButtonStateProvider = installButtonStateProvider,
                        modifier = Modifier.fillMaxSize(),
                    )
                }
            }
        }
    }
}

@Composable
private fun SearchTabPage(
    tab: SearchTabType,
    fossItems: LazyPagingItems<Application>?,
    pwaItems: LazyPagingItems<Application>?,
    playStoreItems: LazyPagingItems<Application>?,
    searchVersion: Int,
    getScrollPosition: (SearchTabType) -> ScrollPosition?,
    onScrollPositionChange: (SearchTabType, Int, Int) -> Unit,
    onResultClick: (Application) -> Unit,
    onPrimaryActionClick: (Application, InstallButtonAction) -> Unit,
    onShowMoreClick: (Application) -> Unit,
    onPrivacyClick: (Application) -> Unit,
    installButtonStateProvider: (Application) -> InstallButtonState,
    modifier: Modifier = Modifier,
) {
    when (tab) {
                SearchTabType.OPEN_SOURCE, SearchTabType.PWA -> {
        SearchTabType.OPEN_SOURCE -> {
            PagingSearchResultList(
                        items = items,
                items = fossItems,
                searchVersion = searchVersion,
                tab = tab,
                getScrollPosition = getScrollPosition,
@@ -146,7 +200,23 @@ fun SearchResultsContent(
                onShowMoreClick = onShowMoreClick,
                onPrivacyClick = onPrivacyClick,
                installButtonStateProvider = installButtonStateProvider,
                        modifier = Modifier.fillMaxSize(),
                modifier = modifier,
            )
        }

        SearchTabType.PWA -> {
            PagingSearchResultList(
                items = pwaItems,
                searchVersion = searchVersion,
                tab = tab,
                getScrollPosition = getScrollPosition,
                onScrollPositionChange = onScrollPositionChange,
                onItemClick = onResultClick,
                onPrimaryActionClick = onPrimaryActionClick,
                onShowMoreClick = onShowMoreClick,
                onPrivacyClick = onPrivacyClick,
                installButtonStateProvider = installButtonStateProvider,
                modifier = modifier,
            )
        }

@@ -161,13 +231,11 @@ fun SearchResultsContent(
                onShowMoreClick = onShowMoreClick,
                onPrivacyClick = onPrivacyClick,
                installButtonStateProvider = installButtonStateProvider,
                        modifier = Modifier.fillMaxSize(),
                modifier = modifier,
            )
        }
    }
}
    }
}

@Composable
private fun PagingPlayStoreResultList(
@@ -207,14 +275,33 @@ private fun PagingPlayStoreResultList(

    val loadState = lazyItems.loadState

    val errorState = loadState.refresh as? LoadState.Error
        ?: loadState.prepend as? LoadState.Error
        ?: loadState.append as? LoadState.Error
    val refreshState = loadState.refresh
    val refreshError = refreshState as? LoadState.Error
    val appendError = loadState.append as? LoadState.Error
    val prependError = loadState.prepend as? LoadState.Error

    val isRefreshing = loadState.refresh is LoadState.Loading
    val isRefreshing = refreshState is LoadState.Loading
    val isAppending = loadState.append is LoadState.Loading
    val isError = errorState != null
    val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0

    val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) }

    LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) {
        if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) {
            hasLoadedCurrentQuery.value = true
        }
        if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) {
            hasLoadedCurrentQuery.value = false
        }
    }

    val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value
    val showFooterError = hasLoadedCurrentQuery.value && listOf(
        refreshError,
        appendError,
        prependError
    ).any { it != null }
    val isEmpty =
        !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0

    Box(modifier = modifier) {
        when {
@@ -222,10 +309,11 @@ private fun PagingPlayStoreResultList(
                SearchShimmerList()
            }

            isError -> {
            initialLoadError -> {
                SearchErrorState(
                    onRetry = { lazyItems.retry() },
                    modifier = Modifier.fillMaxSize()
                    modifier = Modifier.fillMaxSize(),
                    fullScreen = true,
                )
            }

@@ -287,6 +375,18 @@ private fun PagingPlayStoreResultList(
                            }
                        }
                    }

                    if (showFooterError) {
                        item(key = "error_footer_play_store") {
                            SearchErrorState(
                                onRetry = { lazyItems.retry() },
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .padding(horizontal = 16.dp, vertical = 12.dp),
                                fullScreen = false,
                            )
                        }
                    }
                }
            }
        }
@@ -328,14 +428,33 @@ private fun PagingSearchResultList(

    val loadState = lazyItems.loadState

    val errorState = loadState.refresh as? LoadState.Error
        ?: loadState.prepend as? LoadState.Error
        ?: loadState.append as? LoadState.Error
    val refreshState = loadState.refresh
    val refreshError = refreshState as? LoadState.Error
    val appendError = loadState.append as? LoadState.Error
    val prependError = loadState.prepend as? LoadState.Error

    val isRefreshing = loadState.refresh is LoadState.Loading
    val isRefreshing = refreshState is LoadState.Loading
    val isAppending = loadState.append is LoadState.Loading
    val isError = errorState != null
    val isEmpty = !isRefreshing && !isError && lazyItems.itemCount == 0

    val hasLoadedCurrentQuery = remember(searchVersion) { mutableStateOf(false) }

    LaunchedEffect(searchVersion, refreshState, lazyItems.itemCount) {
        if (refreshState is LoadState.NotLoading && lazyItems.itemCount > 0) {
            hasLoadedCurrentQuery.value = true
        }
        if (refreshState is LoadState.Loading && lazyItems.itemCount == 0) {
            hasLoadedCurrentQuery.value = false
        }
    }

    val initialLoadError = refreshError != null && !hasLoadedCurrentQuery.value
    val showFooterError = hasLoadedCurrentQuery.value && listOf(
        refreshError,
        appendError,
        prependError
    ).any { it != null }
    val isEmpty =
        !isRefreshing && refreshError == null && appendError == null && prependError == null && lazyItems.itemCount == 0

    Box(modifier = modifier) {
        when {
@@ -345,10 +464,11 @@ private fun PagingSearchResultList(
                )
            }

            isError -> {
            initialLoadError -> {
                SearchErrorState(
                    onRetry = { lazyItems.retry() },
                    modifier = Modifier.fillMaxSize()
                    modifier = Modifier.fillMaxSize(),
                    fullScreen = true,
                )
            }

@@ -415,6 +535,18 @@ private fun PagingSearchResultList(
                            }
                        }
                    }

                    if (showFooterError) {
                        item(key = "error_footer") {
                            SearchErrorState(
                                onRetry = { lazyItems.retry() },
                                modifier = Modifier
                                    .fillMaxWidth()
                                    .padding(horizontal = 16.dp, vertical = 12.dp),
                                fullScreen = false,
                            )
                        }
                    }
                }
            }
        }
+34 −2
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ package foundation.e.apps.ui.compose.components.search
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -31,22 +32,37 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import foundation.e.apps.R
import foundation.e.apps.ui.compose.theme.AppTheme

@Composable
fun SearchErrorState(
    onRetry: () -> Unit,
    modifier: Modifier = Modifier,
    fullScreen: Boolean = true,
) {
    val containerModifier = if (fullScreen) {
        modifier.fillMaxSize()
    } else {
        modifier.fillMaxWidth()
    }

    val contentPadding = if (fullScreen) {
        PaddingValues(all = 24.dp)
    } else {
        PaddingValues(horizontal = 16.dp, vertical = 12.dp)
    }

    Box(
        modifier = modifier.fillMaxSize(),
        modifier = containerModifier,
        contentAlignment = Alignment.Center
    ) {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(24.dp),
                .padding(contentPadding),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(12.dp),
        ) {
@@ -61,3 +77,19 @@ fun SearchErrorState(
        }
    }
}

@Preview(showBackground = true)
@Composable
private fun SearchErrorStateFullScreenPreview() {
    AppTheme {
        SearchErrorState(onRetry = {}, fullScreen = true)
    }
}

@Preview(showBackground = true)
@Composable
private fun SearchErrorStateFooterPreview() {
    AppTheme {
        SearchErrorState(onRetry = {}, fullScreen = false)
    }
}
+14 −7
Original line number Diff line number Diff line
@@ -113,6 +113,7 @@ class SearchViewModelV2 @Inject constructor(
    private data class SearchRequest(
        val query: String,
        val visibleTabs: List<SearchTabType>,
        val version: Int,
    )

    private val searchRequests = MutableStateFlow<SearchRequest?>(null)
@@ -238,7 +239,9 @@ class SearchViewModelV2 @Inject constructor(
        val selectedTab = _uiState.value.selectedTab?.takeIf { visibleTabs.contains(it) }
            ?: visibleTabs.firstOrNull()

        var nextVersion = _uiState.value.searchVersion + 1
        _uiState.update { current ->
            nextVersion = current.searchVersion + 1
            current.copy(
                query = trimmedQuery,
                suggestions = emptyList(),
@@ -246,14 +249,15 @@ class SearchViewModelV2 @Inject constructor(
                availableTabs = visibleTabs,
                selectedTab = selectedTab,
                hasSubmittedSearch = visibleTabs.isNotEmpty(),
                searchVersion = current.searchVersion + 1,
                searchVersion = nextVersion,
            )
        }

        if (visibleTabs.isNotEmpty()) {
            searchRequests.value = SearchRequest(
                query = trimmedQuery,
                visibleTabs = visibleTabs
                visibleTabs = visibleTabs,
                version = nextVersion,
            )
            _scrollPositions.update { emptyMap() }
        }
@@ -284,17 +288,20 @@ class SearchViewModelV2 @Inject constructor(
            )
        }

        val currentQuery = _uiState.value.query
        val shouldUpdateRequest = _uiState.value.hasSubmittedSearch && currentQuery.isNotBlank()
        val currentState = _uiState.value
        val currentQuery = currentState.query
        val shouldUpdateRequest = currentState.hasSubmittedSearch && currentQuery.isNotBlank()
        if (shouldUpdateRequest && visibleTabs.isNotEmpty()) {
            searchRequests.value = SearchRequest(
                query = currentQuery,
                visibleTabs = visibleTabs
                visibleTabs = visibleTabs,
                version = currentState.searchVersion,
            )
        } else if (!_uiState.value.hasSubmittedSearch) {
        } else if (!currentState.hasSubmittedSearch) {
            searchRequests.value = SearchRequest(
                query = "",
                visibleTabs = visibleTabs
                visibleTabs = visibleTabs,
                version = currentState.searchVersion,
            )
        }
    }