diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-12-18 11:25:28 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2024-01-19 13:31:20 +0100 |
| commit | 93eb40ea81019f4389c894ca30076b8fd4aafb90 (patch) | |
| tree | 91cddd58203eb14459da63211668c7df4f00df81 /android/app/src | |
| parent | bf2d6a6e15a2d405537948d37bdc94b510805f67 (diff) | |
| download | mullvadvpn-93eb40ea81019f4389c894ca30076b8fd4aafb90.tar.xz mullvadvpn-93eb40ea81019f4389c894ca30076b8fd4aafb90.zip | |
Improve handling of empty relay list
Diffstat (limited to 'android/app/src')
5 files changed, 132 insertions, 118 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt index b0157e4e15..0de13b5e6c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt @@ -10,8 +10,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Icon @@ -45,6 +45,7 @@ import net.mullvad.mullvadvpn.compose.component.textResource import net.mullvad.mullvadvpn.compose.constant.ContentType import net.mullvad.mullvadvpn.compose.destinations.FilterScreenDestination import net.mullvad.mullvadvpn.compose.extensions.toAnnotatedString +import net.mullvad.mullvadvpn.compose.state.RelayListState import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState import net.mullvad.mullvadvpn.compose.test.CIRCULAR_PROGRESS_INDICATOR import net.mullvad.mullvadvpn.compose.textfield.SearchTextField @@ -62,12 +63,15 @@ import org.koin.androidx.compose.koinViewModel @Composable private fun PreviewSelectLocationScreen() { val state = - SelectLocationUiState.ShowData( + SelectLocationUiState.Data( searchTerm = "", - countries = listOf(RelayCountry("Country 1", "Code 1", false, emptyList())), - selectedRelay = null, selectedOwnership = null, - selectedProvidersCount = 0 + selectedProvidersCount = 0, + relayListState = + RelayListState.RelayList( + countries = listOf(RelayCountry("Country 1", "Code 1", false, emptyList())), + selectedRelay = null, + ) ) AppTheme { SelectLocationScreen( @@ -141,7 +145,7 @@ fun SelectLocationScreen( when (uiState) { SelectLocationUiState.Loading -> {} - is SelectLocationUiState.ShowData -> { + is SelectLocationUiState.Data -> { if (uiState.hasFilter) { FilterCell( ownershipFilter = uiState.selectedOwnership, @@ -163,12 +167,16 @@ fun SelectLocationScreen( } Spacer(modifier = Modifier.height(height = Dimens.verticalSpace)) val lazyListState = rememberLazyListState() - if (uiState is SelectLocationUiState.ShowData && uiState.selectedRelay != null) { - LaunchedEffect(uiState.selectedRelay) { + if ( + uiState is SelectLocationUiState.Data && + uiState.relayListState is RelayListState.RelayList && + uiState.relayListState.selectedRelay != null + ) { + LaunchedEffect(uiState.relayListState.selectedRelay) { val index = - uiState.countries.indexOfFirst { - it.location.location.country == - uiState.selectedRelay.location.location.country + uiState.relayListState.countries.indexOfFirst { relayCountry -> + relayCountry.location.location.country == + uiState.relayListState.selectedRelay.location.location.country } lazyListState.scrollToItem(index) @@ -187,66 +195,14 @@ fun SelectLocationScreen( ) { when (uiState) { SelectLocationUiState.Loading -> { - item(contentType = ContentType.PROGRESS) { - MullvadCircularProgressIndicatorLarge( - Modifier.testTag(CIRCULAR_PROGRESS_INDICATOR) - ) - } + loading() } - is SelectLocationUiState.ShowData -> { - if (uiState.countries.isEmpty() && uiState.searchTerm.isNotEmpty()) { - item(contentType = ContentType.EMPTY_TEXT) { - val firstRow = - HtmlCompat.fromHtml( - textResource( - id = R.string.select_location_empty_text_first_row, - uiState.searchTerm - ), - HtmlCompat.FROM_HTML_MODE_COMPACT - ) - .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold) - val secondRow = - textResource( - id = R.string.select_location_empty_text_second_row - ) - Column( - modifier = - Modifier.padding( - horizontal = Dimens.selectLocationTitlePadding - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = firstRow, - style = MaterialTheme.typography.labelMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSecondary, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - Text( - text = secondRow, - style = MaterialTheme.typography.labelMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSecondary - ) - } - } - } else { - items( - count = uiState.countries.size, - key = { index -> uiState.countries[index].hashCode() }, - contentType = { ContentType.ITEM } - ) { index -> - val country = uiState.countries[index] - RelayLocationCell( - relay = country, - selectedItem = uiState.selectedRelay, - onSelectRelay = onSelectRelay, - modifier = Modifier.animateContentSize() - ) - } - } + is SelectLocationUiState.Data -> { + relayList( + relayListState = uiState.relayListState, + searchTerm = uiState.searchTerm, + onSelectRelay = onSelectRelay + ) } } } @@ -254,6 +210,71 @@ fun SelectLocationScreen( } } +private fun LazyListScope.loading() { + item(contentType = ContentType.PROGRESS) { + MullvadCircularProgressIndicatorLarge(Modifier.testTag(CIRCULAR_PROGRESS_INDICATOR)) + } +} + +private fun LazyListScope.relayList( + relayListState: RelayListState, + searchTerm: String, + onSelectRelay: (item: RelayItem) -> Unit +) { + when (relayListState) { + is RelayListState.RelayList -> { + items( + count = relayListState.countries.size, + key = { index -> relayListState.countries[index].hashCode() }, + contentType = { ContentType.ITEM } + ) { index -> + val country = relayListState.countries[index] + RelayLocationCell( + relay = country, + selectedItem = relayListState.selectedRelay, + onSelectRelay = onSelectRelay, + modifier = Modifier.animateContentSize() + ) + } + } + RelayListState.Empty -> { + if (searchTerm.isNotEmpty()) + item(contentType = ContentType.EMPTY_TEXT) { + val firstRow = + HtmlCompat.fromHtml( + textResource( + id = R.string.select_location_empty_text_first_row, + searchTerm + ), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold) + val secondRow = + textResource(id = R.string.select_location_empty_text_second_row) + Column( + modifier = Modifier.padding(horizontal = Dimens.selectLocationTitlePadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = firstRow, + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSecondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = secondRow, + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSecondary + ) + } + } + } + } +} + suspend fun LazyListState.animateScrollAndCentralizeItem(index: Int) { val itemInfo = this.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } if (itemInfo != null) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt index 8a65c64b01..da46938e84 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/FilterConstrainExtensions.kt @@ -17,9 +17,9 @@ fun Ownership?.toOwnershipConstraint(): Constraint<Ownership> = else -> Constraint.Only(this) } -fun Constraint<Providers>.toSelectedProviders(allProviders: List<Provider>): List<Provider> = +fun Constraint<Providers>.toSelectedProviders(allProviders: List<Provider>): List<Provider>? = when (this) { - is Constraint.Any -> allProviders + is Constraint.Any -> null is Constraint.Only -> this.value.providers.toList().mapNotNull { providerName -> allProviders.firstOrNull { it.name == providerName } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt index 123bf821e6..3152ed1a34 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt @@ -8,13 +8,19 @@ sealed interface SelectLocationUiState { data object Loading : SelectLocationUiState - data class ShowData( + data class Data( val searchTerm: String, - val countries: List<RelayCountry>, - val selectedRelay: RelayItem?, val selectedOwnership: Ownership?, - val selectedProvidersCount: Int? + val selectedProvidersCount: Int?, + val relayListState: RelayListState ) : SelectLocationUiState { val hasFilter: Boolean = (selectedProvidersCount != null || selectedOwnership != null) } } + +sealed interface RelayListState { + data object Empty : RelayListState + + data class RelayList(val countries: List<RelayCountry>, val selectedRelay: RelayItem?) : + RelayListState +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt index 3f95d79193..bd0703e6ae 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/FilterViewModel.kt @@ -40,6 +40,7 @@ class FilterViewModel( selectedConstraintProviders.toSelectedProviders(allProviders) } .first() + ?: emptyList() val ownershipConstraint = relayListFilterUseCase.selectedOwnership().first() selectedOwnership.value = ownershipConstraint.toNullableOwnership() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt index 70bd92fef7..d3c5977e27 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.RelayListState import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState import net.mullvad.mullvadvpn.compose.state.toNullableOwnership import net.mullvad.mullvadvpn.compose.state.toSelectedProviders @@ -44,35 +45,30 @@ class SelectLocationViewModel( selectedOwnership, allProviders, selectedConstraintProviders -> - val selectedProviders = - selectedConstraintProviders.toSelectedProviders(allProviders) - - val selectedProvidersByOwnershipList = + val selectedOwnershipItem = selectedOwnership.toNullableOwnership() + val selectedProvidersCount = filterSelectedProvidersByOwnership( - selectedProviders, - selectedOwnership.toNullableOwnership() - ) - - val allProvidersByOwnershipListList = - filterAllProvidersByOwnership( - allProviders, - selectedOwnership.toNullableOwnership() - ) + selectedConstraintProviders.toSelectedProviders(allProviders), + selectedOwnershipItem + ) + ?.size val filteredRelayCountries = relayCountries.filterOnSearchTerm(searchTerm, relayItem) - SelectLocationUiState.ShowData( + + SelectLocationUiState.Data( searchTerm = searchTerm, - countries = filteredRelayCountries, - selectedRelay = relayItem, - selectedOwnership = selectedOwnership.toNullableOwnership(), - selectedProvidersCount = - if ( - selectedProvidersByOwnershipList.size == - allProvidersByOwnershipListList.size - ) - null - else selectedProvidersByOwnershipList.size + selectedOwnership = selectedOwnershipItem, + selectedProvidersCount = selectedProvidersCount, + relayListState = + if (filteredRelayCountries.isNotEmpty()) { + RelayListState.RelayList( + countries = filteredRelayCountries, + selectedRelay = relayItem + ) + } else { + RelayListState.Empty + }, ) } .stateIn( @@ -99,26 +95,16 @@ class SelectLocationViewModel( } private fun filterSelectedProvidersByOwnership( - selectedProviders: List<Provider>, + selectedProviders: List<Provider>?, selectedOwnership: Ownership? - ): List<Provider> { - return when (selectedOwnership) { - Ownership.MullvadOwned -> selectedProviders.filter { it.mullvadOwned } - Ownership.Rented -> selectedProviders.filterNot { it.mullvadOwned } - else -> selectedProviders - } - } - - private fun filterAllProvidersByOwnership( - allProviders: List<Provider>, - selectedOwnership: Ownership? - ): List<Provider> { - return when (selectedOwnership) { - Ownership.MullvadOwned -> allProviders.filter { it.mullvadOwned } - Ownership.Rented -> allProviders.filterNot { it.mullvadOwned } - else -> allProviders + ): List<Provider>? = + selectedProviders?.let { + when (selectedOwnership) { + Ownership.MullvadOwned -> selectedProviders.filter { it.mullvadOwned } + Ownership.Rented -> selectedProviders.filterNot { it.mullvadOwned } + else -> selectedProviders + } } - } fun removeOwnerFilter() { viewModelScope.launch { |
