summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-13 14:00:35 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-05-13 14:00:35 +0200
commite1f80b0bfda2a56c1f89beb971b26fbc6faf371e (patch)
tree6c4fa73c3fd7a7be6a9202aaf2216c78fc6ae9a0 /android
parentd36f215dd5170e50cb2c668fc9c42ae7ecaeac2c (diff)
parent365acf9b49139cbdc2afbda524f5fc4f863a726f (diff)
downloadmullvadvpn-e1f80b0bfda2a56c1f89beb971b26fbc6faf371e.tar.xz
mullvadvpn-e1f80b0bfda2a56c1f89beb971b26fbc6faf371e.zip
Merge branch 'search-requires-at-least-2-letters-droid-1605'
Diffstat (limited to 'android')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt112
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt4
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt38
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt109
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/EmptyRelayListText.kt20
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationsEmptyText.kt29
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt62
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ManageDevicesUiStatePreviewParameterProvider.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt51
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt65
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt82
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreen.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt47
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt28
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt46
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt37
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationUiState.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListConstant.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LoadingContentError.kt38
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt101
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt6
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/Expand.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt145
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt72
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt51
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt17
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt69
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt23
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt20
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt14
-rw-r--r--android/lib/resource/src/main/res/values-da/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-de/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-es/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-fi/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-fr/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-it/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-ja/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-ko/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-my/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-nb/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-nl/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-pl/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-pt/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-ru/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-sv/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-th/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-tr/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-zh-rCN/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values-zh-rTW/strings.xml1
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml1
57 files changed, 840 insertions, 575 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
index 049ce6836c..d556fe5d10 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
@@ -12,11 +12,13 @@ import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES
import net.mullvad.mullvadvpn.compose.setContentWithTheme
+import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.ui.tag.CIRCULAR_PROGRESS_INDICATOR_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SAVE_BUTTON_TEST_TAG
+import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
@@ -55,7 +57,9 @@ class CustomListLocationsScreenTest {
fun givenLoadingStateShouldShowLoadingSpinner() =
composeExtension.use {
// Arrange
- initScreen(state = CustomListLocationsUiState.Loading(newList = false))
+ initScreen(
+ state = CustomListLocationsUiState(newList = false, content = Lce.Loading(Unit))
+ )
// Assert
onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR_TEST_TAG).assertExists()
@@ -66,7 +70,9 @@ class CustomListLocationsScreenTest {
composeExtension.use {
// Arrange
val newList = true
- initScreen(state = CustomListLocationsUiState.Loading(newList = newList))
+ initScreen(
+ state = CustomListLocationsUiState(newList = newList, content = Lce.Loading(Unit))
+ )
// Assert
onNodeWithText(ADD_LOCATIONS_TEXT).assertExists()
@@ -77,7 +83,9 @@ class CustomListLocationsScreenTest {
composeExtension.use {
// Arrange
val newList = false
- initScreen(state = CustomListLocationsUiState.Loading(newList = newList))
+ initScreen(
+ state = CustomListLocationsUiState(newList = newList, content = Lce.Loading(Unit))
+ )
// Assert
onNodeWithText(EDIT_LOCATIONS_TEXT).assertExists()
@@ -89,13 +97,27 @@ class CustomListLocationsScreenTest {
// Arrange
initScreen(
state =
- CustomListLocationsUiState.Content.Data(
- locations =
- listOf(
- RelayLocationListItem(DUMMY_RELAY_COUNTRIES[0], checked = true),
- RelayLocationListItem(DUMMY_RELAY_COUNTRIES[1], checked = false),
+ CustomListLocationsUiState(
+ newList = false,
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ locations =
+ listOf(
+ RelayLocationListItem(
+ DUMMY_RELAY_COUNTRIES[0],
+ checked = true,
+ ),
+ RelayLocationListItem(
+ DUMMY_RELAY_COUNTRIES[1],
+ checked = false,
+ ),
+ ),
+ searchTerm = "",
+ saveEnabled = false,
+ hasUnsavedChanges = false,
+ )
),
- searchTerm = "",
)
)
@@ -112,9 +134,20 @@ class CustomListLocationsScreenTest {
val mockedOnRelaySelectionClicked: (RelayItem, Boolean) -> Unit = mockk(relaxed = true)
initScreen(
state =
- CustomListLocationsUiState.Content.Data(
+ CustomListLocationsUiState(
newList = false,
- locations = listOf(RelayLocationListItem(selectedCountry, checked = true)),
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ locations =
+ listOf(
+ RelayLocationListItem(selectedCountry, checked = true)
+ ),
+ searchTerm = "",
+ saveEnabled = false,
+ hasUnsavedChanges = false,
+ )
+ ),
),
onRelaySelectionClick = mockedOnRelaySelectionClicked,
)
@@ -133,9 +166,17 @@ class CustomListLocationsScreenTest {
val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true)
initScreen(
state =
- CustomListLocationsUiState.Content.Data(
+ CustomListLocationsUiState(
newList = false,
- locations = emptyList(),
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ locations = emptyList(),
+ searchTerm = "",
+ saveEnabled = false,
+ hasUnsavedChanges = false,
+ )
+ ),
),
onSearchTermInput = mockedSearchTermInput,
)
@@ -156,9 +197,17 @@ class CustomListLocationsScreenTest {
val mockSearchString = "SEARCH"
initScreen(
state =
- CustomListLocationsUiState.Content.Empty(
+ CustomListLocationsUiState(
newList = false,
- searchTerm = mockSearchString,
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ searchTerm = mockSearchString,
+ saveEnabled = false,
+ hasUnsavedChanges = false,
+ locations = emptyList(),
+ )
+ ),
),
onSearchTermInput = mockedSearchTermInput,
)
@@ -171,13 +220,8 @@ class CustomListLocationsScreenTest {
fun whenRelayListIsEmptyShouldShowNoRelaysText() =
composeExtension.use {
// Arrange
- val emptySearchString = ""
initScreen(
- state =
- CustomListLocationsUiState.Content.Empty(
- newList = false,
- searchTerm = emptySearchString,
- )
+ state = CustomListLocationsUiState(newList = false, content = Lce.Error(Unit))
)
// Assert
@@ -191,10 +235,17 @@ class CustomListLocationsScreenTest {
val mockOnSaveClick: () -> Unit = mockk(relaxed = true)
initScreen(
state =
- CustomListLocationsUiState.Content.Data(
+ CustomListLocationsUiState(
newList = false,
- locations = emptyList(),
- saveEnabled = true,
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ locations = emptyList(),
+ saveEnabled = true,
+ hasUnsavedChanges = true,
+ searchTerm = "",
+ )
+ ),
),
onSaveClick = mockOnSaveClick,
)
@@ -213,10 +264,17 @@ class CustomListLocationsScreenTest {
val mockOnSaveClick: () -> Unit = mockk(relaxed = true)
initScreen(
state =
- CustomListLocationsUiState.Content.Data(
+ CustomListLocationsUiState(
newList = false,
- locations = emptyList(),
- saveEnabled = false,
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ locations = emptyList(),
+ saveEnabled = false,
+ hasUnsavedChanges = false,
+ searchTerm = "",
+ )
+ ),
),
onSaveClick = mockOnSaveClick,
)
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt
index 7059243d81..6fc1ffd5ab 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreenTest.kt
@@ -37,7 +37,7 @@ class ManageDevicesScreenTest {
}
private fun ComposeContext.initScreen(
- state: Lce<ManageDevicesUiState, GetDeviceListError>,
+ state: Lce<Unit, ManageDevicesUiState, GetDeviceListError>,
snackbarHostState: SnackbarHostState = SnackbarHostState(),
onBackClick: () -> Unit = {},
onTryAgainClicked: () -> Unit = {},
@@ -58,7 +58,7 @@ class ManageDevicesScreenTest {
fun loadingStateShowsProgressIndicator() {
composeExtension.use {
// Arrange
- initScreen(state = Lce.Loading)
+ initScreen(state = Lce.Loading(Unit))
// Assert
onNodeWithTag(CIRCULAR_PROGRESS_INDICATOR_TEST_TAG).assertIsDisplayed()
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt
index 805324a74f..32a91fa61b 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt
@@ -18,6 +18,7 @@ import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
+import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -38,7 +39,7 @@ class SearchLocationScreenTest {
}
private fun ComposeContext.initScreen(
- state: SearchLocationUiState,
+ state: Lce<Unit, SearchLocationUiState, Unit>,
onSelectRelay: (RelayItem) -> Unit = {},
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit = { _, _, _ -> },
onSearchInputChanged: (String) -> Unit = {},
@@ -85,7 +86,15 @@ class SearchLocationScreenTest {
// Arrange
val mockedSearchTermInput: (String) -> Unit = mockk(relaxed = true)
initScreen(
- state = SearchLocationUiState.NoQuery(searchTerm = "", filterChips = emptyList()),
+ state =
+ Lce.Content(
+ SearchLocationUiState(
+ searchTerm = "",
+ filterChips = emptyList(),
+ relayListItems = emptyList(),
+ emptyList(),
+ )
+ ),
onSearchInputChanged = mockedSearchTermInput,
)
val mockSearchString = "SEARCH"
@@ -104,11 +113,14 @@ class SearchLocationScreenTest {
val mockSearchString = "SEARCH"
initScreen(
state =
- SearchLocationUiState.Content(
- searchTerm = mockSearchString,
- filterChips = emptyList(),
- relayListItems = listOf(RelayListItem.LocationsEmptyText(mockSearchString)),
- customLists = emptyList(),
+ Lce.Content(
+ SearchLocationUiState(
+ searchTerm = mockSearchString,
+ filterChips = emptyList(),
+ relayListItems =
+ listOf(RelayListItem.LocationsEmptyText(mockSearchString)),
+ customLists = emptyList(),
+ )
)
)
@@ -124,11 +136,13 @@ class SearchLocationScreenTest {
val mockSearchString = "SEARCH"
initScreen(
state =
- SearchLocationUiState.Content(
- searchTerm = mockSearchString,
- filterChips = emptyList(),
- relayListItems = emptyList(),
- customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ Lce.Content(
+ SearchLocationUiState(
+ searchTerm = mockSearchString,
+ filterChips = emptyList(),
+ relayListItems = emptyList(),
+ customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ )
)
)
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt
index 81fa866825..3b259154a4 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt
@@ -24,6 +24,8 @@ import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.performLongClick
+import net.mullvad.mullvadvpn.util.Lc
+import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
@@ -43,7 +45,7 @@ class SelectLocationScreenTest {
fun setup() {
MockKAnnotations.init(this)
loadKoinModules(module { viewModel { listViewModel } })
- every { listViewModel.uiState } returns MutableStateFlow(SelectLocationListUiState.Loading)
+ every { listViewModel.uiState } returns MutableStateFlow(Lce.Loading(Unit))
}
@AfterEach
@@ -52,7 +54,7 @@ class SelectLocationScreenTest {
}
private fun ComposeContext.initScreen(
- state: SelectLocationUiState = SelectLocationUiState.Loading,
+ state: Lc<Unit, SelectLocationUiState> = Lc.Loading(Unit),
onSelectRelay: (item: RelayItem) -> Unit = {},
onSearchClick: (RelayListType) -> Unit = {},
onBackClick: () -> Unit = {},
@@ -104,19 +106,25 @@ class SelectLocationScreenTest {
// Arrange
every { listViewModel.uiState } returns
MutableStateFlow(
- SelectLocationListUiState.Content(
- relayListItems =
- DUMMY_RELAY_COUNTRIES.map { RelayListItem.GeoLocationItem(item = it) },
- customLists = emptyList(),
+ Lce.Content(
+ SelectLocationListUiState(
+ relayListItems =
+ DUMMY_RELAY_COUNTRIES.map {
+ RelayListItem.GeoLocationItem(item = it)
+ },
+ customLists = emptyList(),
+ )
)
)
initScreen(
state =
- SelectLocationUiState.Data(
- // searchTerm = "",
- filterChips = emptyList(),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = emptyList(),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
)
)
@@ -135,17 +143,22 @@ class SelectLocationScreenTest {
// Arrange
every { listViewModel.uiState } returns
MutableStateFlow(
- SelectLocationListUiState.Content(
- relayListItems = listOf(RelayListItem.CustomListFooter(false)),
- customLists = emptyList(),
+ Lce.Content(
+ SelectLocationListUiState(
+ relayListItems = listOf(RelayListItem.CustomListFooter(false)),
+ customLists = emptyList(),
+ )
)
)
initScreen(
state =
- SelectLocationUiState.Data(
- filterChips = emptyList(),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = emptyList(),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
)
)
@@ -160,18 +173,23 @@ class SelectLocationScreenTest {
val customList = DUMMY_RELAY_ITEM_CUSTOM_LISTS[0]
every { listViewModel.uiState } returns
MutableStateFlow(
- SelectLocationListUiState.Content(
- relayListItems = listOf(RelayListItem.CustomListItem(customList)),
- customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ Lce.Content(
+ SelectLocationListUiState(
+ relayListItems = listOf(RelayListItem.CustomListItem(customList)),
+ customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ )
)
)
val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true)
initScreen(
state =
- SelectLocationUiState.Data(
- filterChips = emptyList(),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = emptyList(),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
),
onSelectRelay = mockedOnSelectRelay,
)
@@ -190,19 +208,25 @@ class SelectLocationScreenTest {
val customList = DUMMY_RELAY_ITEM_CUSTOM_LISTS[0]
every { listViewModel.uiState } returns
MutableStateFlow(
- SelectLocationListUiState.Content(
- relayListItems = listOf(RelayListItem.CustomListItem(item = customList)),
- customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ Lce.Content(
+ SelectLocationListUiState(
+ relayListItems =
+ listOf(RelayListItem.CustomListItem(item = customList)),
+ customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ )
)
)
val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true)
initScreen(
state =
- SelectLocationUiState.Data(
- // searchTerm = "",
- filterChips = emptyList(),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Content(
+ SelectLocationUiState(
+ // searchTerm = "",
+ filterChips = emptyList(),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
),
onSelectRelay = mockedOnSelectRelay,
)
@@ -221,18 +245,23 @@ class SelectLocationScreenTest {
val relayItem = DUMMY_RELAY_COUNTRIES[0]
every { listViewModel.uiState } returns
MutableStateFlow(
- SelectLocationListUiState.Content(
- relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)),
- customLists = emptyList(),
+ Lce.Content(
+ SelectLocationListUiState(
+ relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)),
+ customLists = emptyList(),
+ )
)
)
val mockedOnSelectRelay: (RelayItem) -> Unit = mockk(relaxed = true)
initScreen(
state =
- SelectLocationUiState.Data(
- filterChips = emptyList(),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = emptyList(),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
),
onSelectRelay = mockedOnSelectRelay,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/EmptyRelayListText.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/EmptyRelayListText.kt
new file mode 100644
index 0000000000..5e61257d28
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/EmptyRelayListText.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.compose.component
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+
+@Composable
+fun EmptyRelayListText() {
+ Text(
+ text = stringResource(R.string.no_locations_found),
+ modifier = Modifier.padding(Dimens.screenVerticalMargin),
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationsEmptyText.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationsEmptyText.kt
index 579be88bb6..bf3e36f57e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationsEmptyText.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationsEmptyText.kt
@@ -5,31 +5,20 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.relaylist.MIN_SEARCH_LENGTH
@Composable
fun LocationsEmptyText(searchTerm: String) {
- if (searchTerm.length >= MIN_SEARCH_LENGTH) {
- Text(
- text = textResource(R.string.search_location_empty_text, searchTerm),
- style = MaterialTheme.typography.labelMedium,
- textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.padding(Dimens.screenVerticalMargin),
- )
- } else {
- Text(
- text = stringResource(R.string.no_locations_found),
- modifier = Modifier.padding(Dimens.screenVerticalMargin),
- style = MaterialTheme.typography.labelMedium,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
+ Text(
+ text = textResource(R.string.search_location_empty_text, searchTerm),
+ style = MaterialTheme.typography.labelMedium,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(Dimens.screenVerticalMargin),
+ )
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
index 7c4253be50..2598c7a9b2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
@@ -1,43 +1,51 @@
package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.util.Lce
class CustomListLocationUiStatePreviewParameterProvider :
PreviewParameterProvider<CustomListLocationsUiState> {
override val values =
sequenceOf(
- CustomListLocationsUiState.Content.Data(
+ CustomListLocationsUiState(
newList = true,
- locations =
- listOf(
- RelayLocationListItem(
- item =
- RelayItemPreviewData.generateRelayItemCountry(
- name = "A relay",
- cityNames = listOf("City 1", "City 2"),
- relaysPerCity = 2,
- active = true,
- )
- ),
- RelayLocationListItem(
- item =
- RelayItemPreviewData.generateRelayItemCountry(
- name = "Another relay",
- cityNames = listOf("City X", "City Y", "City Z"),
- relaysPerCity = 1,
- active = false,
- )
- .copy(id = GeoLocationId.Country("se"))
- ),
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ locations =
+ listOf(
+ RelayLocationListItem(
+ item =
+ RelayItemPreviewData.generateRelayItemCountry(
+ name = "A relay",
+ cityNames = listOf("City 1", "City 2"),
+ relaysPerCity = 2,
+ active = true,
+ )
+ ),
+ RelayLocationListItem(
+ item =
+ RelayItemPreviewData.generateRelayItemCountry(
+ name = "Another relay",
+ cityNames =
+ listOf("City X", "City Y", "City Z"),
+ relaysPerCity = 1,
+ active = false,
+ )
+ .copy(id = GeoLocationId.Country("se"))
+ ),
+ ),
+ searchTerm = "",
+ saveEnabled = true,
+ hasUnsavedChanges = true,
+ )
),
- searchTerm = "",
- saveEnabled = true,
- hasUnsavedChanges = true,
),
- CustomListLocationsUiState.Content.Empty(newList = false, searchTerm = "searchTerm"),
- CustomListLocationsUiState.Loading(newList = true),
+ CustomListLocationsUiState(newList = false, content = Lce.Error(Unit)),
+ CustomListLocationsUiState(newList = false, content = Lce.Loading(Unit)),
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ManageDevicesUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ManageDevicesUiStatePreviewParameterProvider.kt
index 41c9d14205..37cc988118 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ManageDevicesUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/ManageDevicesUiStatePreviewParameterProvider.kt
@@ -8,7 +8,7 @@ import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
import net.mullvad.mullvadvpn.util.Lce
class ManageDevicesUiStatePreviewParameterProvider :
- PreviewParameterProvider<Lce<ManageDevicesUiState, GetDeviceListError>> {
+ PreviewParameterProvider<Lce<Unit, ManageDevicesUiState, GetDeviceListError>> {
override val values =
sequenceOf(
Lce.Content(
@@ -24,7 +24,7 @@ class ManageDevicesUiStatePreviewParameterProvider :
)
),
Lce.Content(ManageDevicesUiState(emptyList())),
- Lce.Loading,
+ Lce.Loading(Unit),
Lce.Error(GetDeviceListError.Unknown(IllegalStateException("Error"))),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt
index 2c695764d7..58bea4810e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt
@@ -49,14 +49,14 @@ object RelayListItemPreviewData {
val locations =
listOf(
RelayItemPreviewData.generateRelayItemCountry(
- name = "A relay",
- cityNames = listOf("City 1", "City 2"),
+ name = "First Country",
+ cityNames = listOf("Capital City", "Minor City"),
relaysPerCity = 2,
active = true,
),
RelayItemPreviewData.generateRelayItemCountry(
- name = "Another relay",
- cityNames = listOf("City X", "City Y", "City Z"),
+ name = "Second Country",
+ cityNames = listOf("Medium City", "Small City", "Vivec City"),
relaysPerCity = 1,
active = false,
),
@@ -92,7 +92,7 @@ object RelayListItemPreviewData {
state = RelayListItemState.USED_AS_EXIT,
),
RelayListItem.GeoLocationItem(
- item = locations[0].cities[1].relays[0],
+ item = locations[0].cities[1].relays[1],
isSelected = false,
depth = 2,
expanded = false,
@@ -109,5 +109,12 @@ object RelayListItemPreviewData {
)
}
- fun generateEmptyList(searchTerm: String) = listOf(RelayListItem.LocationsEmptyText(searchTerm))
+ fun generateEmptyList(searchTerm: String, isSearching: Boolean) =
+ listOf(
+ if (isSearching) {
+ RelayListItem.LocationsEmptyText(searchTerm)
+ } else {
+ RelayListItem.EmptyRelayList
+ }
+ )
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
index ebed8d229f..50cf464bb5 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
@@ -3,27 +3,46 @@ package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.usecase.FilterChip
+import net.mullvad.mullvadvpn.util.Lce
class SearchLocationsUiStatePreviewParameterProvider :
- PreviewParameterProvider<SearchLocationUiState> {
+ PreviewParameterProvider<Lce<Unit, SearchLocationUiState, Unit>> {
override val values =
sequenceOf(
- SearchLocationUiState.NoQuery(searchTerm = "", filterChips = listOf(FilterChip.Entry)),
- SearchLocationUiState.Content(
- searchTerm = "Mullvad",
- filterChips = listOf(FilterChip.Entry),
- relayListItems = RelayListItemPreviewData.generateEmptyList("Mullvad"),
- customLists = emptyList(),
+ Lce.Loading(Unit),
+ Lce.Content(
+ SearchLocationUiState(
+ searchTerm = "",
+ filterChips = listOf(FilterChip.Entry),
+ relayListItems =
+ RelayListItemPreviewData.generateRelayListItems(
+ includeCustomLists = true,
+ isSearching = true,
+ ),
+ customLists = emptyList(),
+ )
),
- SearchLocationUiState.Content(
- searchTerm = "Germany",
- filterChips = listOf(FilterChip.Entry),
- relayListItems =
- RelayListItemPreviewData.generateRelayListItems(
- includeCustomLists = true,
- isSearching = true,
- ),
- customLists = emptyList(),
+ Lce.Error(Unit),
+ Lce.Content(
+ SearchLocationUiState(
+ searchTerm = "Mullvad",
+ filterChips = listOf(FilterChip.Entry),
+ relayListItems =
+ RelayListItemPreviewData.generateEmptyList("Mullvad", isSearching = true),
+ customLists = emptyList(),
+ )
+ ),
+ Lce.Content(
+ SearchLocationUiState(
+ searchTerm = "Germany",
+ filterChips = listOf(FilterChip.Entry),
+ relayListItems =
+ RelayListItemPreviewData.generateRelayListItems(
+ includeCustomLists = true,
+ isSearching = true,
+ ),
+ customLists = emptyList(),
+ )
),
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt
index 965999c7f1..99bb6993ef 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt
@@ -5,39 +5,52 @@ import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.usecase.ModelOwnership
+import net.mullvad.mullvadvpn.util.Lc
class SelectLocationsUiStatePreviewParameterProvider :
- PreviewParameterProvider<SelectLocationUiState> {
+ PreviewParameterProvider<Lc<Unit, SelectLocationUiState>> {
override val values =
sequenceOf(
- SelectLocationUiState.Loading,
- SelectLocationUiState.Data(
- filterChips = emptyList(),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Loading(Unit),
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = emptyList(),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
),
- SelectLocationUiState.Data(
- filterChips =
- listOf(
- FilterChip.Ownership(ownership = ModelOwnership.Rented),
- FilterChip.Provider(PROVIDER_COUNT),
- ),
- multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips =
+ listOf(
+ FilterChip.Ownership(ownership = ModelOwnership.Rented),
+ FilterChip.Provider(PROVIDER_COUNT),
+ ),
+ multihopEnabled = false,
+ relayListType = RelayListType.EXIT,
+ isTopBarActionsEnabled = true,
+ )
),
- SelectLocationUiState.Data(
- filterChips = emptyList(),
- multihopEnabled = true,
- relayListType = RelayListType.ENTRY,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = emptyList(),
+ multihopEnabled = true,
+ relayListType = RelayListType.ENTRY,
+ isTopBarActionsEnabled = true,
+ )
),
- SelectLocationUiState.Data(
- filterChips =
- listOf(
- FilterChip.Ownership(ownership = ModelOwnership.MullvadOwned),
- FilterChip.Provider(PROVIDER_COUNT),
- ),
- multihopEnabled = true,
- relayListType = RelayListType.ENTRY,
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips =
+ listOf(
+ FilterChip.Ownership(ownership = ModelOwnership.MullvadOwned),
+ FilterChip.Provider(PROVIDER_COUNT),
+ ),
+ multihopEnabled = true,
+ relayListType = RelayListType.ENTRY,
+ isTopBarActionsEnabled = true,
+ )
),
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
index a8b1194f3f..5e302c6990 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.screen
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -34,6 +35,7 @@ import com.ramcosta.composedestinations.result.ResultRecipient
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.cell.CheckableRelayLocationCell
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
+import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
@@ -44,6 +46,7 @@ import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.dialog.info.Confirmed
import net.mullvad.mullvadvpn.compose.extensions.animateScrollAndCentralizeItem
import net.mullvad.mullvadvpn.compose.preview.CustomListLocationUiStatePreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.textfield.SearchTextField
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
@@ -55,6 +58,7 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.ui.tag.SAVE_BUTTON_TEST_TAG
+import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsSideEffect
import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsViewModel
import org.koin.androidx.compose.koinViewModel
@@ -100,7 +104,7 @@ fun CustomListLocations(
onExpand = customListsViewModel::onExpand,
onBackClick =
dropUnlessResumed {
- if (state.hasUnsavedChanges) {
+ if (state.content.contentOrNull()?.hasUnsavedChanges == true) {
navigator.navigate(DiscardChangesDestination)
} else {
backNavigator.navigateBack()
@@ -118,6 +122,8 @@ fun CustomListLocationsScreen(
onExpand: (RelayItem.Location, selected: Boolean) -> Unit,
onBackClick: () -> Unit,
) {
+ BackHandler(onBack = onBackClick)
+
ScaffoldWithSmallTopBar(
appBarTitle =
stringResource(
@@ -128,7 +134,12 @@ fun CustomListLocationsScreen(
}
),
navigationIcon = { NavigateBackIconButton(onNavigateBack = onBackClick) },
- actions = { Actions(isSaveEnabled = state.saveEnabled, onSaveClick = onSaveClick) },
+ actions = {
+ Actions(
+ isSaveEnabled = state.content.contentOrNull()?.saveEnabled == true,
+ onSaveClick = onSaveClick,
+ )
+ },
) { modifier ->
Column(modifier = modifier) {
SearchTextField(
@@ -154,16 +165,16 @@ fun CustomListLocationsScreen(
.fillMaxWidth(),
state = lazyListState,
) {
- when (state) {
- is CustomListLocationsUiState.Loading -> {
+ when (state.content) {
+ is Lce.Loading -> {
loading()
}
- is CustomListLocationsUiState.Content.Empty -> {
- empty(searchTerm = state.searchTerm)
+ is Lce.Error -> {
+ empty()
}
- is CustomListLocationsUiState.Content.Data -> {
+ is Lce.Content -> {
content(
- uiState = state,
+ uiState = state.content.value,
onRelaySelectedChanged = onRelaySelectionClick,
onExpand = onExpand,
)
@@ -171,8 +182,8 @@ fun CustomListLocationsScreen(
}
}
- if (state is CustomListLocationsUiState.Content.Data && !state.newList) {
- val firstChecked = state.locations.indexOfFirst { it.checked }
+ if (state.content is Lce.Content && !state.newList) {
+ val firstChecked = state.content.value.locations.indexOfFirst { it.checked }
LaunchedEffect(Unit) {
if (firstChecked != -1) {
lazyListState.scrollToItem(firstChecked)
@@ -204,33 +215,48 @@ private fun LazyListScope.loading() {
}
}
-private fun LazyListScope.empty(searchTerm: String) {
+private fun LazyListScope.empty() {
item(key = CommonContentKey.EMPTY, contentType = ContentType.EMPTY_TEXT) {
- LocationsEmptyText(searchTerm = searchTerm)
+ EmptyRelayListText()
}
}
private fun LazyListScope.content(
- uiState: CustomListLocationsUiState.Content.Data,
+ uiState: CustomListLocationsData,
onExpand: (RelayItem.Location, expand: Boolean) -> Unit,
onRelaySelectedChanged: (RelayItem.Location, selected: Boolean) -> Unit,
) {
- itemsIndexed(uiState.locations, key = { index, listItem -> listItem.item.id }) { index, listItem
- ->
- Column(modifier = Modifier.animateItem()) {
- if (index != 0) {
- HorizontalDivider()
+ if (uiState.locations.isEmpty()) {
+ item(key = CommonContentKey.EMPTY, contentType = ContentType.EMPTY_TEXT) {
+ LocationsEmptyText(searchTerm = uiState.searchTerm)
+ }
+ } else {
+ itemsIndexed(uiState.locations, key = { index, listItem -> listItem.item.id }) {
+ index,
+ listItem ->
+ Column(modifier = Modifier.animateItem()) {
+ if (index != 0) {
+ HorizontalDivider()
+ }
+ CheckableRelayLocationCell(
+ item = listItem.item,
+ onRelayCheckedChange = { isChecked ->
+ onRelaySelectedChanged(listItem.item, isChecked)
+ },
+ checked = listItem.checked,
+ depth = listItem.depth,
+ onExpand = { expand -> onExpand(listItem.item, expand) },
+ expanded = listItem.expanded,
+ )
}
- CheckableRelayLocationCell(
- item = listItem.item,
- onRelayCheckedChange = { isChecked ->
- onRelaySelectedChanged(listItem.item, isChecked)
- },
- checked = listItem.checked,
- depth = listItem.depth,
- onExpand = { expand -> onExpand(listItem.item, expand) },
- expanded = listItem.expanded,
- )
}
}
}
+
+private fun Lce<Boolean, CustomListLocationsUiState, Boolean>.newList(): Boolean {
+ return when (this) {
+ is Lce.Content -> this.value.newList
+ is Lce.Loading -> this.value
+ is Lce.Error -> this.error
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreen.kt
index d6a4e38256..dc36921b9a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ManageDevicesScreen.kt
@@ -54,12 +54,12 @@ import org.koin.androidx.compose.koinViewModel
@Preview("Normal|TooMany|Empty|Loading|Error")
private fun PreviewDeviceListScreenContent(
@PreviewParameter(ManageDevicesUiStatePreviewParameterProvider::class)
- state: Lce<ManageDevicesUiState, GetDeviceListError>
+ state: Lce<Unit, ManageDevicesUiState, GetDeviceListError>
) {
AppTheme { ManageDevicesScreen(state = state, SnackbarHostState(), {}, {}, {}) }
}
-private typealias StateLce = Lce<ManageDevicesUiState, GetDeviceListError>
+private typealias StateLce = Lce<Unit, ManageDevicesUiState, GetDeviceListError>
@Destination<RootGraph>(style = DefaultTransition::class, navArgs = DeviceListNavArgs::class)
@Composable
@@ -121,7 +121,7 @@ fun ManageDevicesScreen(
is Lce.Content ->
Content(modifier, state.value, navigateToRemoveDeviceConfirmationDialog)
is Lce.Error -> Error(modifier, onTryAgainClicked)
- Lce.Loading -> Loading(modifier)
+ is Lce.Loading -> Loading(modifier)
}
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt
index 249093329e..0b5617fc7e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt
@@ -17,6 +17,7 @@ import net.mullvad.mullvadvpn.compose.cell.HeaderCell
import net.mullvad.mullvadvpn.compose.cell.StatusRelayItemCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
import net.mullvad.mullvadvpn.compose.cell.ThreeDotCell
+import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsEntryBottomSheet
@@ -102,6 +103,7 @@ fun LazyListScope.relayListContent(
{ expand -> onToggleExpand(listItem.item.id, null, expand) },
)
is RelayListItem.LocationsEmptyText -> LocationsEmptyText(listItem.searchTerm)
+ is RelayListItem.EmptyRelayList -> EmptyRelayListText()
}
}
},
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt
index be5aa0d306..785739dc61 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt
@@ -37,7 +37,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -56,6 +55,8 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.cell.FilterRow
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
+import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
+import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.MullvadSnackbar
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.constant.ContentType
@@ -73,15 +74,16 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.usecase.FilterChip
+import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.location.SearchLocationSideEffect
import net.mullvad.mullvadvpn.viewmodel.location.SearchLocationViewModel
import org.koin.androidx.compose.koinViewModel
-@Preview("Default|Not found|Results")
+@Preview("Loading|Default|No Locations|Not found|Results")
@Composable
private fun PreviewSearchLocationScreen(
@PreviewParameter(SearchLocationsUiStatePreviewParameterProvider::class)
- state: SearchLocationUiState
+ state: Lce<Unit, SearchLocationUiState, Unit>
) {
AppTheme {
SearchLocationScreen(
@@ -218,7 +220,7 @@ fun SearchLocation(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchLocationScreen(
- state: SearchLocationUiState,
+ state: Lce<Unit, SearchLocationUiState, Unit>,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
onSelectRelay: (RelayItem) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
@@ -262,7 +264,8 @@ fun SearchLocationScreen(
LaunchedEffect(Unit) { focusRequester.requestFocus() }
SearchBar(
modifier = Modifier.focusRequester(focusRequester),
- searchTerm = state.searchTerm,
+ searchTerm = state.contentOrNull()?.searchTerm ?: "",
+ enabled = state is Lce.Content,
backgroundColor = backgroundColor,
onBackgroundColor = onBackgroundColor,
onSearchInputChanged = onSearchInputChanged,
@@ -283,20 +286,24 @@ fun SearchLocationScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
filterRow(
- filters = state.filterChips,
+ filters = state.contentOrNull()?.filterChips ?: emptyList(),
onBackgroundColor = onBackgroundColor,
onRemoveOwnershipFilter = onRemoveOwnershipFilter,
onRemoveProviderFilter = onRemoveProviderFilter,
)
when (state) {
- is SearchLocationUiState.NoQuery -> {
- noQuery()
+ is Lce.Loading -> {
+ loading()
}
- is SearchLocationUiState.Content -> {
+ is Lce.Error -> {
+ // Relay list is empty
+ item { EmptyRelayListText() }
+ }
+ is Lce.Content -> {
relayListContent(
backgroundColor = backgroundColor,
- customLists = state.customLists,
- relayListItems = state.relayListItems,
+ customLists = state.value.customLists,
+ relayListItems = state.value.relayListItems,
onSelectRelay = onSelectRelay,
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = { newSheetState ->
@@ -326,6 +333,7 @@ fun SearchLocationScreen(
@Composable
private fun SearchBar(
searchTerm: String,
+ enabled: Boolean,
backgroundColor: Color,
onBackgroundColor: Color,
onSearchInputChanged: (String) -> Unit,
@@ -336,6 +344,7 @@ private fun SearchBar(
SearchBarDefaults.InputField(
modifier = modifier.height(Dimens.searchFieldHeightExpanded).fillMaxWidth(),
query = searchTerm,
+ enabled = enabled,
onQueryChange = onSearchInputChanged,
onSearch = { hideKeyboard() },
expanded = true,
@@ -376,18 +385,6 @@ private fun SearchBar(
)
}
-private fun LazyListScope.noQuery() {
- item(contentType = ContentType.DESCRIPTION) {
- Text(
- text = stringResource(R.string.search_query_empty),
- style = MaterialTheme.typography.labelMedium,
- textAlign = TextAlign.Center,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- modifier = Modifier.padding(Dimens.mediumPadding),
- )
- }
-}
-
private fun LazyListScope.filterRow(
filters: List<FilterChip>,
onBackgroundColor: Color,
@@ -420,3 +417,7 @@ private fun Title(text: String, onBackgroundColor: Color) {
style = MaterialTheme.typography.labelMedium,
)
}
+
+private fun LazyListScope.loading() {
+ item(contentType = ContentType.PROGRESS) { MullvadCircularProgressIndicatorLarge() }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt
index 33113b03c9..d42177dbac 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt
@@ -33,10 +33,15 @@ import net.mullvad.mullvadvpn.compose.util.RunOnKeyChange
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
+import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
+private typealias EntryBlocked = Lce.Error<Unit>
+
+private typealias Content = Lce.Content<SelectLocationListUiState>
+
@Composable
fun SelectLocationList(
backgroundColor: Color,
@@ -53,7 +58,7 @@ fun SelectLocationList(
val state by viewModel.uiState.collectAsStateWithLifecycle()
val lazyListState = rememberLazyListState()
val stateActual = state
- RunOnKeyChange(stateActual is SelectLocationListUiState.Content) {
+ RunOnKeyChange(stateActual is Content) {
stateActual.indexOfSelectedRelayItem()?.let { index ->
lazyListState.scrollToItem(index)
lazyListState.animateScrollAndCentralizeItem(index)
@@ -69,24 +74,24 @@ fun SelectLocationList(
state = lazyListState,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement =
- if (state is SelectLocationListUiState.EntryBlocked) {
+ if (state is EntryBlocked) {
Arrangement.Center
} else {
Arrangement.Top
},
) {
when (stateActual) {
- SelectLocationListUiState.Loading -> {
+ is Lce.Loading -> {
loading()
}
- SelectLocationListUiState.EntryBlocked -> {
+ is EntryBlocked -> {
entryBlocked(openDaitaSettings = openDaitaSettings)
}
- is SelectLocationListUiState.Content -> {
+ is Content -> {
relayListContent(
backgroundColor = backgroundColor,
- relayListItems = stateActual.relayListItems,
- customLists = stateActual.customLists,
+ relayListItems = stateActual.value.relayListItems,
+ customLists = stateActual.value.customLists,
onSelectRelay = onSelectRelay,
onToggleExpand = viewModel::onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
@@ -129,10 +134,10 @@ private fun LazyListScope.entryBlocked(openDaitaSettings: () -> Unit) {
}
}
-private fun SelectLocationListUiState.indexOfSelectedRelayItem(): Int? =
- if (this is SelectLocationListUiState.Content) {
+private fun Lce<Unit, SelectLocationListUiState, Unit>.indexOfSelectedRelayItem(): Int? =
+ if (this is Content) {
val index =
- relayListItems.indexOfFirst {
+ value.relayListItems.indexOfFirst {
when (it) {
is RelayListItem.CustomListItem -> it.isSelected
is RelayListItem.GeoLocationItem -> it.isSelected
@@ -140,7 +145,8 @@ private fun SelectLocationListUiState.indexOfSelectedRelayItem(): Int? =
is RelayListItem.CustomListFooter,
RelayListItem.CustomListHeader,
RelayListItem.LocationHeader,
- is RelayListItem.LocationsEmptyText -> false
+ is RelayListItem.LocationsEmptyText,
+ is RelayListItem.EmptyRelayList -> false
}
}
if (index >= 0) index else null
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
index b41a95d64e..d6d3742162 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
@@ -73,7 +73,10 @@ import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled
+import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_SCREEN_TEST_TAG
+import net.mullvad.mullvadvpn.util.Lc
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationSideEffect
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationViewModel
import org.koin.androidx.compose.koinViewModel
@@ -82,7 +85,7 @@ import org.koin.androidx.compose.koinViewModel
@Composable
private fun PreviewSelectLocationScreen(
@PreviewParameter(SelectLocationsUiStatePreviewParameterProvider::class)
- state: SelectLocationUiState
+ state: Lc<Unit, SelectLocationUiState>
) {
AppTheme {
SelectLocationScreen(
@@ -227,7 +230,7 @@ fun SelectLocation(
@Suppress("LongMethod", "LongParameterList")
@Composable
fun SelectLocationScreen(
- state: SelectLocationUiState,
+ state: Lc<Unit, SelectLocationUiState>,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
onSelectRelay: (item: RelayItem) -> Unit,
onSearchClick: (RelayListType) -> Unit,
@@ -261,23 +264,28 @@ fun SelectLocationScreen(
modifier = Modifier.testTag(SELECT_LOCATION_SCREEN_TEST_TAG),
snackbarHostState = snackbarHostState,
actions = {
+ val isTopBarActionsEnabled = state.contentOrNull()?.isTopBarActionsEnabled == true
IconButton(
- enabled = state is SelectLocationUiState.Data,
- onClick = {
- if (state is SelectLocationUiState.Data) onSearchClick(state.relayListType)
- },
+ enabled = isTopBarActionsEnabled,
+ onClick = { state.contentOrNull()?.let { onSearchClick(it.relayListType) } },
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search),
- tint = MaterialTheme.colorScheme.onSurface,
+ tint =
+ MaterialTheme.colorScheme.onSurface.copy(
+ alpha = if (isTopBarActionsEnabled) AlphaVisible else AlphaDisabled
+ ),
)
}
- IconButton(enabled = state is SelectLocationUiState.Data, onClick = onFilterClick) {
+ IconButton(enabled = isTopBarActionsEnabled, onClick = onFilterClick) {
Icon(
imageVector = Icons.Default.FilterList,
contentDescription = stringResource(id = R.string.filter),
- tint = MaterialTheme.colorScheme.onSurface,
+ tint =
+ MaterialTheme.colorScheme.onSurface.copy(
+ alpha = if (isTopBarActionsEnabled) AlphaVisible else AlphaDisabled
+ ),
)
}
},
@@ -299,17 +307,17 @@ fun SelectLocationScreen(
modifier = modifier.background(backgroundColor).fillMaxSize(),
verticalArrangement =
when (state) {
- SelectLocationUiState.Loading -> Arrangement.Center
- is SelectLocationUiState.Data -> Arrangement.Top
+ is Lc.Loading -> Arrangement.Center
+ is Lc.Content -> Arrangement.Top
},
) {
when (state) {
- SelectLocationUiState.Loading -> {
+ is Lc.Loading -> {
Loading()
}
- is SelectLocationUiState.Data -> {
+ is Lc.Content -> {
AnimatedContent(
- targetState = state.filterChips,
+ targetState = state.value.filterChips,
label = "Select location top bar",
) { filterChips ->
if (filterChips.isNotEmpty()) {
@@ -321,16 +329,16 @@ fun SelectLocationScreen(
}
}
- if (state.multihopEnabled) {
- MultihopBar(state.relayListType, onSelectRelayList)
+ if (state.value.multihopEnabled) {
+ MultihopBar(state.value.relayListType, onSelectRelayList)
}
- if (state.filterChips.isNotEmpty() || state.multihopEnabled) {
+ if (state.value.filterChips.isNotEmpty() || state.value.multihopEnabled) {
Spacer(modifier = Modifier.height(height = Dimens.verticalSpace))
}
RelayLists(
- state = state,
+ state = state.value,
backgroundColor = backgroundColor,
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
@@ -365,7 +373,7 @@ private fun MultihopBar(relayListType: RelayListType, onSelectRelayList: (RelayL
@Composable
private fun RelayLists(
- state: SelectLocationUiState.Data,
+ state: SelectLocationUiState,
backgroundColor: Color,
onSelectRelay: (RelayItem) -> Unit,
openDaitaSettings: () -> Unit,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
index 8a3c5c756b..9f8eae8c53 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
@@ -1,34 +1,19 @@
package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.util.Lce
-sealed interface CustomListLocationsUiState {
- val newList: Boolean
- val saveEnabled: Boolean
- val hasUnsavedChanges: Boolean
-
- data class Loading(override val newList: Boolean = false) : CustomListLocationsUiState {
- override val saveEnabled: Boolean = false
- override val hasUnsavedChanges: Boolean = false
- }
-
- sealed interface Content : CustomListLocationsUiState {
- val searchTerm: String
-
- data class Empty(override val newList: Boolean, override val searchTerm: String) : Content {
- override val saveEnabled: Boolean = false
- override val hasUnsavedChanges: Boolean = false
- }
+data class CustomListLocationsUiState(
+ val newList: Boolean,
+ val content: Lce<Unit, CustomListLocationsData, Unit>,
+)
- data class Data(
- override val newList: Boolean = false,
- val locations: List<RelayLocationListItem>,
- override val searchTerm: String = "",
- override val saveEnabled: Boolean = false,
- override val hasUnsavedChanges: Boolean = false,
- ) : Content
- }
-}
+data class CustomListLocationsData(
+ val saveEnabled: Boolean,
+ val hasUnsavedChanges: Boolean,
+ val searchTerm: String,
+ val locations: List<RelayLocationListItem>,
+)
data class RelayLocationListItem(
val item: RelayItem.Location,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt
index 34fd369526..028912f15d 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt
@@ -12,6 +12,7 @@ enum class RelayListItemContentType {
LOCATION_HEADER,
LOCATION_ITEM,
LOCATIONS_EMPTY_TEXT,
+ EMPTY_RELAY_LIST,
}
enum class RelayListItemState {
@@ -86,4 +87,9 @@ sealed interface RelayListItem {
override val key = "locations_empty_text"
override val contentType = RelayListItemContentType.LOCATIONS_EMPTY_TEXT
}
+
+ data object EmptyRelayList : RelayListItem {
+ override val key = "empty_relay_list"
+ override val contentType = RelayListItemContentType.EMPTY_RELAY_LIST
+ }
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
index fd35213dac..c377be8814 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
@@ -3,19 +3,9 @@ package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.usecase.FilterChip
-sealed interface SearchLocationUiState {
- val searchTerm: String
- val filterChips: List<FilterChip>
-
- data class NoQuery(
- override val searchTerm: String,
- override val filterChips: List<FilterChip>,
- ) : SearchLocationUiState
-
- data class Content(
- override val searchTerm: String,
- override val filterChips: List<FilterChip>,
- val relayListItems: List<RelayListItem>,
- val customLists: List<RelayItem.CustomList>,
- ) : SearchLocationUiState
-}
+data class SearchLocationUiState(
+ val searchTerm: String,
+ val filterChips: List<FilterChip>,
+ val relayListItems: List<RelayListItem>,
+ val customLists: List<RelayItem.CustomList>,
+)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt
index d470187bcf..393286a35e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt
@@ -2,14 +2,7 @@ package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
-sealed interface SelectLocationListUiState {
-
- data object Loading : SelectLocationListUiState
-
- data object EntryBlocked : SelectLocationListUiState
-
- data class Content(
- val relayListItems: List<RelayListItem>,
- val customLists: List<RelayItem.CustomList>,
- ) : SelectLocationListUiState
-}
+data class SelectLocationListUiState(
+ val relayListItems: List<RelayListItem>,
+ val customLists: List<RelayItem.CustomList>,
+)
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 fd2abab8c4..6f986f2916 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
@@ -2,12 +2,9 @@ package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.usecase.FilterChip
-sealed interface SelectLocationUiState {
- data object Loading : SelectLocationUiState
-
- data class Data(
- val filterChips: List<FilterChip>,
- val multihopEnabled: Boolean,
- val relayListType: RelayListType,
- ) : SelectLocationUiState
-}
+data class SelectLocationUiState(
+ val filterChips: List<FilterChip>,
+ val multihopEnabled: Boolean,
+ val relayListType: RelayListType,
+ val isTopBarActionsEnabled: Boolean,
+)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt
index ac03080e21..fac0e77321 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt
@@ -13,7 +13,7 @@ fun CustomList.toRelayItemCustomList(
)
fun List<RelayItem.CustomList>.filterOnSearchTerm(searchTerm: String) =
- if (searchTerm.length >= MIN_SEARCH_LENGTH) {
+ if (searchTerm.isNotEmpty()) {
this.filter { it.name.contains(searchTerm, ignoreCase = true) }
} else {
this
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListConstant.kt
deleted file mode 100644
index 4fcc5c7902..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListConstant.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package net.mullvad.mullvadvpn.relaylist
-
-const val MIN_SEARCH_LENGTH = 2
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LoadingContentError.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LoadingContentError.kt
index c16aa9242f..d3823e1386 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LoadingContentError.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LoadingContentError.kt
@@ -1,39 +1,25 @@
package net.mullvad.mullvadvpn.util
-sealed interface Lce<out T, out E> {
- data object Loading : Lce<Nothing, Nothing>
+sealed interface Lce<out L, out T, out E> {
+ data class Loading<L>(val value: L) : Lce<L, Nothing, Nothing>
- data class Content<T>(val value: T) : Lce<T, Nothing>
+ data class Content<T>(val value: T) : Lce<Nothing, T, Nothing>
- data class Error<E>(val error: E) : Lce<Nothing, E>
+ data class Error<E>(val error: E) : Lce<Nothing, Nothing, E>
- fun contentOrNull(): T? =
- when (this) {
- is Loading,
- is Error -> null
- is Content -> value
- }
+ fun contentOrNull(): T? = (this as? Content<T>)?.value
- fun errorOrNull(): E? =
- when (this) {
- is Loading,
- is Content -> null
- is Error -> error
- }
+ fun errorOrNull(): E? = (this as? Error<E>)?.error
}
-fun <T, E> T.toLce(): Lce<T, E> = Lce.Content(this)
+fun <L, T, E> T.toLce(): Lce<L, T, E> = Lce.Content(this)
-sealed interface Lc<out T> {
- data object Loading : Lc<Nothing>
+sealed interface Lc<out L, out T> {
+ data class Loading<L>(val value: L) : Lc<L, Nothing>
- data class Content<T>(val value: T) : Lc<T>
+ data class Content<T>(val value: T) : Lc<Nothing, T>
- fun contentOrNull(): T? =
- when (this) {
- is Content -> value
- Loading -> null
- }
+ fun contentOrNull(): T? = (this as? Content<T>)?.value
}
-fun <T> T.toLc(): Lc<T> = Lc.Content(this)
+fun <L, T> T.toLc(): Lc<L, T> = Lc.Content(this)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
index 320420369d..baadf379cb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
@@ -12,8 +12,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -21,11 +19,11 @@ import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
+import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
-import net.mullvad.mullvadvpn.relaylist.MIN_SEARCH_LENGTH
import net.mullvad.mullvadvpn.relaylist.ancestors
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch
@@ -33,7 +31,9 @@ import net.mullvad.mullvadvpn.relaylist.withDescendants
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase
+import net.mullvad.mullvadvpn.util.Lce
+@Suppress("TooManyFunctions")
class CustomListLocationsViewModel(
private val relayListRepository: RelayListRepository,
private val customListRelayItemsUseCase: CustomListRelayItemsUseCase,
@@ -48,62 +48,72 @@ class CustomListLocationsViewModel(
private val _initialLocations = MutableStateFlow<Set<RelayItem.Location>>(emptySet())
private val _selectedLocations = MutableStateFlow<Set<RelayItem.Location>?>(null)
private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM)
- private val _expandedItems = MutableStateFlow<Set<RelayItemId>>(setOf())
+ private val _expandOverrides = MutableStateFlow<Map<RelayItemId, Boolean>>(mapOf())
val uiState =
- combine(searchRelayListLocations(), _searchTerm, _selectedLocations, _expandedItems) {
- relayCountries,
+ combine(_searchTerm, relayListRepository.relayList, _selectedLocations, _expandOverrides) {
searchTerm,
+ relayCountries,
selectedLocations,
- expandedLocations ->
+ expandOverrides ->
when {
selectedLocations == null ->
- CustomListLocationsUiState.Loading(newList = navArgs.newList)
+ CustomListLocationsUiState(
+ newList = navArgs.newList,
+ content = Lce.Loading(Unit),
+ )
relayCountries.isEmpty() ->
- CustomListLocationsUiState.Content.Empty(
+ CustomListLocationsUiState(
newList = navArgs.newList,
- searchTerm = searchTerm,
+ content = Lce.Error(Unit),
)
- else ->
- CustomListLocationsUiState.Content.Data(
+ else -> {
+ val (expandSet, filteredRelayCountries) =
+ searchRelayListLocations(searchTerm, relayCountries)
+ val expandedLocations = expandSet.with(expandOverrides)
+ CustomListLocationsUiState(
newList = navArgs.newList,
- searchTerm = searchTerm,
- locations =
- relayCountries.toRelayItems(
- isSelected = { it in selectedLocations },
- isExpanded = { it in expandedLocations },
+ content =
+ Lce.Content(
+ CustomListLocationsData(
+ searchTerm = searchTerm,
+ locations =
+ filteredRelayCountries.toRelayItems(
+ isSelected = { it in selectedLocations },
+ isExpanded = { it in expandedLocations },
+ ),
+ saveEnabled =
+ selectedLocations.isNotEmpty() &&
+ selectedLocations != _initialLocations.value,
+ hasUnsavedChanges =
+ selectedLocations != _initialLocations.value,
+ )
),
- saveEnabled =
- selectedLocations.isNotEmpty() &&
- selectedLocations != _initialLocations.value,
- hasUnsavedChanges = selectedLocations != _initialLocations.value,
)
+ }
}
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
- CustomListLocationsUiState.Loading(newList = navArgs.newList),
+ CustomListLocationsUiState(newList = navArgs.newList, content = Lce.Loading(Unit)),
)
init {
viewModelScope.launch { fetchInitialSelectedLocations() }
}
- private fun searchRelayListLocations() =
- combine(_searchTerm, relayListRepository.relayList) { searchTerm, relayCountries ->
- val isSearching = searchTerm.length >= MIN_SEARCH_LENGTH
- if (isSearching) {
- val (exp, filteredRelayCountries) = relayCountries.newFilterOnSearch(searchTerm)
- exp.toSet() to filteredRelayCountries
- } else {
- initialExpands(
- _selectedLocations.value?.calculateLocationsToSave() ?: emptyList()
- ) to relayCountries
- }
- }
- .onEach { _expandedItems.value = it.first }
- .map { it.second }
+ private fun searchRelayListLocations(
+ searchTerm: String,
+ relayCountries: List<RelayItem.Location.Country>,
+ ) =
+ if (searchTerm.isNotEmpty()) {
+ val (exp, filteredRelayCountries) = relayCountries.newFilterOnSearch(searchTerm)
+ exp.toSet() to filteredRelayCountries
+ } else {
+ initialExpands(_selectedLocations.value?.calculateLocationsToSave() ?: emptyList()) to
+ relayCountries
+ }
fun save() {
viewModelScope.launch {
@@ -138,17 +148,14 @@ class CustomListLocationsViewModel(
}
fun onExpand(relayItem: RelayItem.Location, expand: Boolean) {
- _expandedItems.update {
- if (expand) {
- it + relayItem.id
- } else {
- it - relayItem.id
- }
- }
+ _expandOverrides.update { it + (relayItem.id to expand) }
}
fun onSearchTermInput(searchTerm: String) {
- viewModelScope.launch { _searchTerm.emit(searchTerm) }
+ viewModelScope.launch {
+ _expandOverrides.emit(emptyMap())
+ _searchTerm.emit(searchTerm)
+ }
}
private fun selectLocation(relayItem: RelayItem.Location) {
@@ -227,7 +234,7 @@ class CustomListLocationsViewModel(
_initialLocations.value = selectedLocations
_selectedLocations.value = selectedLocations
// Initial expand
- _expandedItems.value = initialExpands(locations)
+ _expandOverrides.value = initialExpands(locations).associate { it to true }
}
private fun initialExpands(locations: List<RelayItem.Location>): Set<RelayItemId> =
@@ -307,6 +314,10 @@ class CustomListLocationsViewModel(
}
}
+ private fun Set<RelayItemId>.with(overrides: Map<RelayItemId, Boolean>): Set<RelayItemId> =
+ this + overrides.filterValues { expanded -> expanded }.keys -
+ overrides.filterValues { expanded -> !expanded }.keys
+
companion object {
private const val EMPTY_SEARCH_TERM = ""
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt
index 53533264f1..b92fd9845b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModel.kt
@@ -31,13 +31,13 @@ class ManageDevicesViewModel(
.filter { it is DeviceListSideEffect.FailedToRemoveDevice }
.map { ManageDevicesSideEffect.FailedToRemoveDevice }
- val uiState: StateFlow<Lce<ManageDevicesUiState, GetDeviceListError>> =
+ val uiState: StateFlow<Lce<Unit, ManageDevicesUiState, GetDeviceListError>> =
combine(
deviceRepository.deviceState.filterIsInstance<DeviceState.LoggedIn>(),
deviceListViewModel.uiState,
) { loggedInState, deviceListState ->
when (deviceListState) {
- DeviceListUiState.Loading -> Lce.Loading
+ DeviceListUiState.Loading -> Lce.Loading(Unit)
is DeviceListUiState.Error -> Lce.Error(deviceListState.error)
is DeviceListUiState.Content -> {
ManageDevicesUiState(
@@ -49,7 +49,7 @@ class ManageDevicesViewModel(
}
}
}
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lce.Loading)
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lce.Loading(Unit))
fun fetchDevices() = deviceListViewModel.fetchDevices()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/Expand.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/Expand.kt
index b517619e6b..726aa79674 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/Expand.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/Expand.kt
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.update
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItemId
-internal fun MutableStateFlow<Set<String>>.onToggleExpand(
+internal fun MutableStateFlow<Set<String>>.onToggleExpandSet(
item: RelayItemId,
parent: CustomListId? = null,
expand: Boolean,
@@ -19,3 +19,14 @@ internal fun MutableStateFlow<Set<String>>.onToggleExpand(
}
}
}
+
+internal fun MutableStateFlow<Map<String, Boolean>>.onToggleExpandMap(
+ item: RelayItemId,
+ parent: CustomListId? = null,
+ expand: Boolean,
+) {
+ update {
+ val key = item.expandKey(parent)
+ it + (key to expand)
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
index 6bd1e31573..d80b8fc548 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt
@@ -8,11 +8,29 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.model.RelayItemSelection
-import net.mullvad.mullvadvpn.relaylist.MIN_SEARCH_LENGTH
import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm
// Creates a relay list to be displayed by RelayListContent
internal fun relayListItems(
+ relayListType: RelayListType,
+ relayCountries: List<RelayItem.Location.Country>,
+ customLists: List<RelayItem.CustomList>,
+ selectedByThisEntryExitList: RelayItemId?,
+ selectedByOtherEntryExitList: RelayItemId?,
+ expandedItems: Set<String>,
+): List<RelayListItem> {
+ return createRelayListItems(
+ relayListType = relayListType,
+ selectedByThisEntryExitList = selectedByThisEntryExitList,
+ selectedByOtherEntryExitList = selectedByOtherEntryExitList,
+ customLists = customLists,
+ countries = relayCountries,
+ ) {
+ it in expandedItems
+ }
+}
+
+internal fun relayListItemsSearching(
searchTerm: String = "",
relayListType: RelayListType,
relayCountries: List<RelayItem.Location.Country>,
@@ -23,28 +41,35 @@ internal fun relayListItems(
): List<RelayListItem> {
val filteredCustomLists = customLists.filterOnSearchTerm(searchTerm)
- return buildList {
- val relayItems =
- createRelayListItems(
- isSearching = searchTerm.isSearching(),
- relayListType = relayListType,
- selectedByThisEntryExitList = selectedByThisEntryExitList,
- selectedByOtherEntryExitList = selectedByOtherEntryExitList,
- customLists = filteredCustomLists,
- countries = relayCountries,
- ) {
- it in expandedItems
- }
- if (relayItems.isEmpty()) {
- add(RelayListItem.LocationsEmptyText(searchTerm))
- } else {
- addAll(relayItems)
+ return createRelayListItemsSearching(
+ relayListType = relayListType,
+ selectedByThisEntryExitList = selectedByThisEntryExitList,
+ selectedByOtherEntryExitList = selectedByOtherEntryExitList,
+ customLists = filteredCustomLists,
+ countries = relayCountries,
+ ) {
+ it in expandedItems
}
- }
+ .ifEmpty { listOf(RelayListItem.LocationsEmptyText(searchTerm)) }
}
+internal fun emptyLocationsRelayListItems(
+ relayListType: RelayListType,
+ customLists: List<RelayItem.CustomList>,
+ selectedByThisEntryExitList: RelayItemId?,
+ selectedByOtherEntryExitList: RelayItemId?,
+ expandedItems: Set<String>,
+) =
+ createCustomListSection(
+ relayListType,
+ selectedByThisEntryExitList,
+ selectedByOtherEntryExitList,
+ customLists,
+ ) {
+ it in expandedItems
+ } + RelayListItem.LocationHeader + RelayListItem.EmptyRelayList
+
private fun createRelayListItems(
- isSearching: Boolean,
relayListType: RelayListType,
selectedByThisEntryExitList: RelayItemId?,
selectedByOtherEntryExitList: RelayItemId?,
@@ -53,7 +78,6 @@ private fun createRelayListItems(
isExpanded: (String) -> Boolean,
): List<RelayListItem> =
createCustomListSection(
- isSearching,
relayListType,
selectedByThisEntryExitList,
selectedByOtherEntryExitList,
@@ -61,7 +85,29 @@ private fun createRelayListItems(
isExpanded,
) +
createLocationSection(
- isSearching,
+ selectedByThisEntryExitList,
+ relayListType,
+ selectedByOtherEntryExitList,
+ countries,
+ isExpanded,
+ )
+
+private fun createRelayListItemsSearching(
+ relayListType: RelayListType,
+ selectedByThisEntryExitList: RelayItemId?,
+ selectedByOtherEntryExitList: RelayItemId?,
+ customLists: List<RelayItem.CustomList>,
+ countries: List<RelayItem.Location.Country>,
+ isExpanded: (String) -> Boolean,
+): List<RelayListItem> =
+ createCustomListSectionSearching(
+ relayListType,
+ selectedByThisEntryExitList,
+ selectedByOtherEntryExitList,
+ customLists,
+ isExpanded,
+ ) +
+ createLocationSectionSearching(
selectedByThisEntryExitList,
relayListType,
selectedByOtherEntryExitList,
@@ -70,16 +116,33 @@ private fun createRelayListItems(
)
private fun createCustomListSection(
- isSearching: Boolean,
relayListType: RelayListType,
selectedByThisEntryExitList: RelayItemId?,
selectedByOtherEntryExitList: RelayItemId?,
customLists: List<RelayItem.CustomList>,
isExpanded: (String) -> Boolean,
): List<RelayListItem> = buildList {
- if (isSearching && customLists.isEmpty()) {
- // If we are searching and no results are found don't show header or footer
- } else {
+ add(RelayListItem.CustomListHeader)
+ val customListItems =
+ createCustomListRelayItems(
+ customLists,
+ relayListType,
+ selectedByThisEntryExitList,
+ selectedByOtherEntryExitList,
+ isExpanded,
+ )
+ addAll(customListItems)
+ add(RelayListItem.CustomListFooter(customListItems.isNotEmpty()))
+}
+
+private fun createCustomListSectionSearching(
+ relayListType: RelayListType,
+ selectedByThisEntryExitList: RelayItemId?,
+ selectedByOtherEntryExitList: RelayItemId?,
+ customLists: List<RelayItem.CustomList>,
+ isExpanded: (String) -> Boolean,
+): List<RelayListItem> = buildList {
+ if (customLists.isNotEmpty()) {
add(RelayListItem.CustomListHeader)
val customListItems =
createCustomListRelayItems(
@@ -90,10 +153,6 @@ private fun createCustomListSection(
isExpanded,
)
addAll(customListItems)
- // Do not show the footer in the search view
- if (!isSearching) {
- add(RelayListItem.CustomListFooter(customListItems.isNotEmpty()))
- }
}
}
@@ -138,16 +197,34 @@ private fun createCustomListRelayItems(
}
private fun createLocationSection(
- isSearching: Boolean,
selectedByThisEntryExitList: RelayItemId?,
relayListType: RelayListType,
selectedByOtherEntryExitList: RelayItemId?,
countries: List<RelayItem.Location.Country>,
isExpanded: (String) -> Boolean,
): List<RelayListItem> = buildList {
- if (isSearching && countries.isEmpty()) {
- // If we are searching and no results are found don't show header or footer
- } else {
+ add(RelayListItem.LocationHeader)
+ addAll(
+ countries.flatMap { country ->
+ createGeoLocationEntry(
+ item = country,
+ selectedByThisEntryExitList = selectedByThisEntryExitList,
+ relayListType = relayListType,
+ selectedByOtherEntryExitList = selectedByOtherEntryExitList,
+ isExpanded = isExpanded,
+ )
+ }
+ )
+}
+
+private fun createLocationSectionSearching(
+ selectedByThisEntryExitList: RelayItemId?,
+ relayListType: RelayListType,
+ selectedByOtherEntryExitList: RelayItemId?,
+ countries: List<RelayItem.Location.Country>,
+ isExpanded: (String) -> Boolean,
+): List<RelayListItem> = buildList {
+ if (countries.isNotEmpty()) {
add(RelayListItem.LocationHeader)
addAll(
countries.flatMap { country ->
@@ -328,8 +405,6 @@ private fun RelayItemId?.singleRelayId(customLists: List<RelayItem.CustomList>):
else -> null
}
-private fun String.isSearching() = length >= MIN_SEARCH_LENGTH
-
private fun RelayItem.createState(
relayListType: RelayListType,
selectedByOtherId: RelayItemId?,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
index 74cecbfdda..5310fe5ca8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt
@@ -9,8 +9,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -22,7 +20,6 @@ import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
-import net.mullvad.mullvadvpn.relaylist.MIN_SEARCH_LENGTH
import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
@@ -35,17 +32,18 @@ import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseCase
+import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.util.combine
@Suppress("LongParameterList")
class SearchLocationViewModel(
private val wireguardConstraintsRepository: WireguardConstraintsRepository,
private val relayListRepository: RelayListRepository,
- private val filteredRelayListUseCase: FilteredRelayListUseCase,
private val customListActionUseCase: CustomListActionUseCase,
private val customListsRepository: CustomListsRepository,
private val relayListFilterRepository: RelayListFilterRepository,
private val filterChipUseCase: FilterChipUseCase,
+ filteredRelayListUseCase: FilteredRelayListUseCase,
filteredCustomListRelayItemsUseCase: FilterCustomListsRelayItemUseCase,
selectedLocationUseCase: SelectedLocationUseCase,
customListsRelayItemUseCase: CustomListsRelayItemUseCase,
@@ -56,17 +54,17 @@ class SearchLocationViewModel(
SearchLocationDestination.argsFrom(savedStateHandle).relayListType
private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM)
- private val _expandedItems = MutableStateFlow<Set<String>>(emptySet())
+ private val _expandOverrides = MutableStateFlow<Map<String, Boolean>>(emptyMap())
- val uiState: StateFlow<SearchLocationUiState> =
+ val uiState: StateFlow<Lce<Unit, SearchLocationUiState, Unit>> =
combine(
_searchTerm,
- searchRelayListLocations(),
+ filteredRelayListUseCase(relayListType),
filteredCustomListRelayItemsUseCase(relayListType = relayListType),
customListsRelayItemUseCase(),
selectedLocationUseCase(),
filterChips(),
- _expandedItems,
+ _expandOverrides,
) {
searchTerm,
relayCountries,
@@ -74,14 +72,23 @@ class SearchLocationViewModel(
customLists,
selectedItem,
filterChips,
- expandedItems ->
- if (searchTerm.length >= MIN_SEARCH_LENGTH) {
- SearchLocationUiState.Content(
+ expandOverrides ->
+ if (relayCountries.isEmpty()) {
+ return@combine Lce.Error<Unit>(Unit)
+ }
+ val (expandSet, relayListLocations) =
+ searchRelayListLocations(
+ searchTerm = searchTerm,
+ relayCountries = relayCountries,
+ )
+ val expandedItems = expandSet.with(expandOverrides)
+ Lce.Content(
+ SearchLocationUiState(
searchTerm = searchTerm,
relayListItems =
- relayListItems(
+ relayListItemsSearching(
searchTerm = searchTerm,
- relayCountries = relayCountries,
+ relayCountries = relayListLocations,
relayListType = relayListType,
customLists = filteredCustomLists,
selectedByThisEntryExitList =
@@ -96,21 +103,18 @@ class SearchLocationViewModel(
customLists = customLists,
filterChips = filterChips,
)
- } else {
- SearchLocationUiState.NoQuery(searchTerm, filterChips)
- }
+ )
}
- .stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(),
- SearchLocationUiState.NoQuery("", emptyList()),
- )
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Lce.Loading(Unit))
private val _uiSideEffect = Channel<SearchLocationSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
fun onSearchInputUpdated(searchTerm: String) {
- viewModelScope.launch { _searchTerm.emit(searchTerm) }
+ viewModelScope.launch {
+ _expandOverrides.emit(emptyMap())
+ _searchTerm.emit(searchTerm)
+ }
}
fun selectRelay(relayItem: RelayItem) {
@@ -128,14 +132,16 @@ class SearchLocationViewModel(
}
}
- private fun searchRelayListLocations() =
- combine(_searchTerm, filteredRelayListUseCase(relayListType)) { searchTerm, relayCountries
- ->
- val (exp, filteredRelayCountries) = relayCountries.newFilterOnSearch(searchTerm)
- exp.map { it.expandKey() }.toSet() to filteredRelayCountries
- }
- .onEach { _expandedItems.value = it.first }
- .map { it.second }
+ private fun searchRelayListLocations(
+ searchTerm: String,
+ relayCountries: List<RelayItem.Location.Country>,
+ ) =
+ if (searchTerm.isNotEmpty()) {
+ val (exp, filteredRelayCountries) = relayCountries.newFilterOnSearch(searchTerm)
+ exp.map { it.expandKey() }.toSet() to filteredRelayCountries
+ } else {
+ emptySet<String>() to relayCountries
+ }
private fun filterChips() =
combine(
@@ -193,9 +199,13 @@ class SearchLocationViewModel(
}
fun onToggleExpand(item: RelayItemId, parent: CustomListId? = null, expand: Boolean) {
- _expandedItems.onToggleExpand(item = item, parent = parent, expand = expand)
+ _expandOverrides.onToggleExpandMap(item = item, parent = parent, expand = expand)
}
+ private fun Set<String>.with(overrides: Map<String, Boolean>): Set<String> =
+ this + overrides.filterValues { expanded -> expanded }.keys -
+ overrides.filterValues { expanded -> !expanded }.keys
+
companion object {
private const val EMPTY_SEARCH_TERM = ""
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
index 46d8ac519d..846a56cdaf 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt
@@ -20,6 +20,7 @@ import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseCase
+import net.mullvad.mullvadvpn.util.Lce
class SelectLocationListViewModel(
private val relayListType: RelayListType,
@@ -34,25 +35,27 @@ class SelectLocationListViewModel(
private val _expandedItems: MutableStateFlow<Set<String>> =
MutableStateFlow(initialExpand(initialSelection()))
- val uiState: StateFlow<SelectLocationListUiState> =
+ val uiState: StateFlow<Lce<Unit, SelectLocationListUiState, Unit>> =
combine(
relayListItems(),
customListsRelayItemUseCase(),
settingsRepository.settingsUpdates,
) { relayListItems, customLists, settings ->
if (relayListType == RelayListType.ENTRY && settings?.entryBlocked() == true) {
- SelectLocationListUiState.EntryBlocked
+ Lce.Error(Unit)
} else {
- SelectLocationListUiState.Content(
- relayListItems = relayListItems,
- customLists = customLists,
+ Lce.Content(
+ SelectLocationListUiState(
+ relayListItems = relayListItems,
+ customLists = customLists,
+ )
)
}
}
- .stateIn(viewModelScope, SharingStarted.Lazily, SelectLocationListUiState.Loading)
+ .stateIn(viewModelScope, SharingStarted.Lazily, Lce.Loading(Unit))
fun onToggleExpand(item: RelayItemId, parent: CustomListId? = null, expand: Boolean) {
- _expandedItems.onToggleExpand(item, parent, expand)
+ _expandedItems.onToggleExpandSet(item, parent, expand)
}
private fun relayListItems() =
@@ -62,16 +65,30 @@ class SelectLocationListViewModel(
selectedLocationUseCase(),
_expandedItems,
) { relayCountries, customLists, selectedItem, expandedItems ->
- relayListItems(
- relayCountries = relayCountries,
- relayListType = relayListType,
- customLists = customLists,
- selectedByThisEntryExitList =
- selectedItem.selectedByThisEntryExitList(relayListType),
- selectedByOtherEntryExitList =
- selectedItem.selectedByOtherEntryExitList(relayListType, customLists),
- expandedItems = expandedItems,
- )
+ // If we have no locations we have an empty relay list
+ // and we should show an error
+ if (relayCountries.isEmpty()) {
+ emptyLocationsRelayListItems(
+ relayListType = relayListType,
+ customLists = customLists,
+ selectedByThisEntryExitList =
+ selectedItem.selectedByThisEntryExitList(relayListType),
+ selectedByOtherEntryExitList =
+ selectedItem.selectedByOtherEntryExitList(relayListType, customLists),
+ expandedItems = expandedItems,
+ )
+ } else {
+ relayListItems(
+ relayCountries = relayCountries,
+ relayListType = relayListType,
+ customLists = customLists,
+ selectedByThisEntryExitList =
+ selectedItem.selectedByThisEntryExitList(relayListType),
+ selectedByOtherEntryExitList =
+ selectedItem.selectedByOtherEntryExitList(relayListType, customLists),
+ expandedItems = expandedItems,
+ )
+ }
}
private fun initialExpand(item: RelayItemId?): Set<String> = buildSet {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
index bf129044ad..f1a9240187 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt
@@ -24,6 +24,7 @@ import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.util.Lc
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("TooManyFunctions")
@@ -43,14 +44,18 @@ class SelectLocationViewModel(
filterChips(),
wireguardConstraintsRepository.wireguardConstraints,
_relayListType,
- ) { filterChips, wireguardConstraints, relayListSelection ->
- SelectLocationUiState.Data(
- filterChips = filterChips,
- multihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
- relayListType = relayListSelection,
+ relayListRepository.relayList,
+ ) { filterChips, wireguardConstraints, relayListSelection, relayList ->
+ Lc.Content(
+ SelectLocationUiState(
+ filterChips = filterChips,
+ multihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
+ relayListType = relayListSelection,
+ isTopBarActionsEnabled = relayList.isNotEmpty(),
+ )
)
}
- .stateIn(viewModelScope, SharingStarted.Lazily, SelectLocationUiState.Loading)
+ .stateIn(viewModelScope, SharingStarted.Lazily, Lc.Loading(Unit))
private val _uiSideEffect = Channel<SelectLocationSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
index 865b4ce471..edb4a9adfe 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
@@ -13,6 +13,7 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.screen.CustomListLocationsNavArgs
+import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
@@ -29,6 +30,7 @@ import net.mullvad.mullvadvpn.relaylist.withDescendants
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListRelayItemsUseCase
+import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -59,10 +61,14 @@ class CustomListLocationsViewModelTest {
name = CustomListName.fromString("name"),
locations = emptyList(),
)
+ relayListFlow.value = DUMMY_COUNTRIES
val viewModel = createViewModel(customListId = customList.id, newList = newList)
// Act, Assert
- viewModel.uiState.test { assertEquals(newList, awaitItem().newList) }
+ viewModel.uiState.test {
+ val state = awaitItem()
+ assertEquals(newList, state.newList)
+ }
}
@Test
@@ -80,7 +86,17 @@ class CustomListLocationsViewModelTest {
}
val customListId = CustomListId("id")
val expectedState =
- CustomListLocationsUiState.Content.Data(newList = true, locations = expectedList)
+ CustomListLocationsUiState(
+ newList = true,
+ Lce.Content(
+ CustomListLocationsData(
+ saveEnabled = false,
+ hasUnsavedChanges = false,
+ searchTerm = "",
+ locations = expectedList,
+ )
+ ),
+ )
val viewModel = createViewModel(customListId, true)
relayListFlow.value = DUMMY_COUNTRIES
@@ -102,8 +118,8 @@ class CustomListLocationsViewModelTest {
viewModel.uiState.test {
// Check no selected
val firstState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(firstState)
- assertEquals(emptyList<RelayItem>(), firstState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
+ assertEquals(emptyList<RelayItem>(), firstState.content.selectedLocations())
// Expand country
viewModel.onExpand(DUMMY_COUNTRIES[0], true)
awaitItem()
@@ -114,8 +130,8 @@ class CustomListLocationsViewModelTest {
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], true)
// Check all items selected
val secondState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(secondState)
- assertLists(expectedSelection, secondState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
+ assertLists(expectedSelection, secondState.content.selectedLocations())
}
}
@@ -133,21 +149,15 @@ class CustomListLocationsViewModelTest {
// Act, Assert
viewModel.uiState.test {
- awaitItem()
- // Expand country
- viewModel.onExpand(DUMMY_COUNTRIES[0], true)
- awaitItem()
- // Expand city
- viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true)
// Check initial selected
val firstState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(firstState)
- assertEquals(initialSelectionIds, firstState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
+ assertEquals(initialSelectionIds, firstState.content.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], false)
// Check all items selected
val secondState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(secondState)
- assertEquals(expectedSelection, secondState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
+ assertEquals(expectedSelection, secondState.content.selectedLocations())
}
}
@@ -166,21 +176,14 @@ class CustomListLocationsViewModelTest {
// Act, Assert
viewModel.uiState.test {
- awaitItem()
- // Expand country
- viewModel.onExpand(DUMMY_COUNTRIES[0], true)
- awaitItem()
- // Expand city
- viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true)
- // Check initial selected
val firstState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(firstState)
- assertEquals(initialSelectionIds, firstState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
+ assertEquals(initialSelectionIds, firstState.content.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], false)
// Check all items selected
val secondState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(secondState)
- assertEquals(expectedSelection, secondState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
+ assertEquals(expectedSelection, secondState.content.selectedLocations())
}
}
@@ -203,13 +206,13 @@ class CustomListLocationsViewModelTest {
viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true)
// Check no selected
val firstState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(firstState)
- assertEquals(emptyList<RelayItem>(), firstState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
+ assertEquals(emptyList<RelayItem>(), firstState.content.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], true)
// Check all items selected
val secondState = awaitItem()
- assertIs<CustomListLocationsUiState.Content.Data>(secondState)
- assertEquals(expectedSelection, secondState.selectedLocations())
+ assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
+ assertEquals(expectedSelection, secondState.content.selectedLocations())
}
}
@@ -323,8 +326,8 @@ class CustomListLocationsViewModelTest {
)
}
- private fun CustomListLocationsUiState.Content.Data.selectedLocations() =
- this.locations.filter { it.checked }.map { it.item.id }
+ private fun Lce.Content<CustomListLocationsData>.selectedLocations() =
+ this.value.locations.filter { it.checked }.map { it.item.id }
private fun RelayItem.Location.toDepth() =
when (this) {
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt
index ac5446cc07..7f4c6fe690 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/ManageDevicesViewModelTest.kt
@@ -78,7 +78,7 @@ class ManageDevicesViewModelTest {
@Test
fun `initial state should be Loading followed by Content`() = runTest {
// Initial state is Loading
- assertIs<Lce.Loading>(viewModel.uiState.value)
+ assertIs<Lce.Loading<Unit>>(viewModel.uiState.value)
viewModel.uiState.test {
val contentState = awaitItem()
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt
index f9c4cace6a..0166bafa98 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt
@@ -29,6 +29,7 @@ import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseCase
+import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -96,20 +97,17 @@ class SearchLocationViewModelTest {
// Act, Assert
viewModel.uiState.test {
// Wait for first data
- assertIs<SearchLocationUiState.NoQuery>(awaitItem())
+ awaitItem()
// Update search string
viewModel.onSearchInputUpdated(mockSearchString)
- // We get some unnecessary emissions for now
- awaitItem()
-
val actualState = awaitItem()
- assertIs<SearchLocationUiState.Content>(actualState)
+ assertIs<Lce.Content<SearchLocationUiState>>(actualState)
assertTrue(
- actualState.relayListItems.filterIsInstance<RelayListItem.GeoLocationItem>().any {
- it.item is RelayItem.Location.City && it.item.name == "Gothenburg"
- }
+ actualState.value.relayListItems
+ .filterIsInstance<RelayListItem.GeoLocationItem>()
+ .any { it.item is RelayItem.Location.City && it.item.name == "Gothenburg" }
)
}
}
@@ -123,20 +121,17 @@ class SearchLocationViewModelTest {
// Act, Assert
viewModel.uiState.test {
// Wait for first data
- assertIs<SearchLocationUiState.NoQuery>(awaitItem())
+ awaitItem()
// Update search string
viewModel.onSearchInputUpdated(mockSearchString)
- // We get some unnecessary emissions for now
- awaitItem()
-
// Assert
val actualState = awaitItem()
- assertIs<SearchLocationUiState.Content>(actualState)
+ assertIs<Lce.Content<SearchLocationUiState>>(actualState)
assertLists(
listOf(RelayListItem.LocationsEmptyText(mockSearchString)),
- actualState.relayListItems,
+ actualState.value.relayListItems,
)
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
index acdf3f5c95..46994ead49 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt
@@ -23,6 +23,7 @@ import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseCase
+import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
@@ -70,7 +71,7 @@ class SelectLocationListViewModelTest {
viewModel = createSelectLocationListViewModel(relayListType = RelayListType.ENTRY)
// Assert
- assertEquals(SelectLocationListUiState.Loading, viewModel.uiState.value)
+ assertEquals(Lce.Loading(Unit), viewModel.uiState.value)
}
@Test
@@ -84,13 +85,13 @@ class SelectLocationListViewModelTest {
// Act, Assert
viewModel.uiState.test {
val actualState = awaitItem()
- assertIs<SelectLocationListUiState.Content>(actualState)
+ assertIs<Lce.Content<SelectLocationListUiState>>(actualState)
assertLists(
testCountries.map { it.id },
- actualState.relayListItems.mapNotNull { it.relayItemId() },
+ actualState.value.relayListItems.mapNotNull { it.relayItemId() },
)
assertTrue(
- actualState.relayListItems
+ actualState.value.relayListItems
.filterIsInstance<RelayListItem.SelectableItem>()
.first { it.relayItemId() == selectedId }
.isSelected
@@ -108,15 +109,15 @@ class SelectLocationListViewModelTest {
// Act, Assert
viewModel.uiState.test {
val actualState = awaitItem()
- assertIs<SelectLocationListUiState.Content>(actualState)
+ assertIs<Lce.Content<SelectLocationListUiState>>(actualState)
assertLists(
testCountries.map { it.id },
- actualState.relayListItems.mapNotNull { it.relayItemId() },
+ actualState.value.relayListItems.mapNotNull { it.relayItemId() },
)
assertTrue(
- actualState.relayListItems.filterIsInstance<RelayListItem.SelectableItem>().all {
- !it.isSelected
- }
+ actualState.value.relayListItems
+ .filterIsInstance<RelayListItem.SelectableItem>()
+ .all { !it.isSelected }
)
}
}
@@ -139,6 +140,7 @@ class SelectLocationListViewModelTest {
RelayListItem.CustomListHeader -> null
RelayListItem.LocationHeader -> null
is RelayListItem.LocationsEmptyText -> null
+ is RelayListItem.EmptyRelayList -> null
is RelayListItem.CustomListEntryItem -> item.id
is RelayListItem.CustomListItem -> item.id
is RelayListItem.GeoLocationItem -> item.id
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
index c79d158f82..a7ecbe17f9 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt
@@ -38,8 +38,8 @@ import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
+import net.mullvad.mullvadvpn.util.Lc
import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -59,6 +59,7 @@ class SelectLocationViewModelTest {
private val selectedRelayItemFlow = MutableStateFlow<Constraint<RelayItemId>>(Constraint.Any)
private val wireguardConstraints = MutableStateFlow<WireguardConstraints>(mockk(relaxed = true))
private val filterChips = MutableStateFlow<List<FilterChip>>(emptyList())
+ private val relayList = MutableStateFlow<List<RelayItem.Location.Country>>(emptyList())
@BeforeEach
fun setup() {
@@ -67,6 +68,7 @@ class SelectLocationViewModelTest {
every { mockWireguardConstraintsRepository.wireguardConstraints } returns
wireguardConstraints
every { mockFilterChipUseCase(any()) } returns filterChips
+ every { mockRelayListRepository.relayList } returns relayList
mockkStatic(RELAY_LIST_EXTENSIONS)
mockkStatic(RELAY_ITEM_EXTENSIONS)
@@ -90,7 +92,7 @@ class SelectLocationViewModelTest {
@Test
fun `initial state should be correct`() = runTest {
- Assertions.assertEquals(SelectLocationUiState.Loading, viewModel.uiState.value)
+ assertIs<Lc.Loading<Unit>>(viewModel.uiState.value)
}
@Test
@@ -128,14 +130,14 @@ class SelectLocationViewModelTest {
viewModel.selectRelayList(RelayListType.ENTRY)
// Assert relay list type is entry
val firstState = awaitItem()
- assertIs<SelectLocationUiState.Data>(firstState)
- assertEquals(RelayListType.ENTRY, firstState.relayListType)
+ assertIs<Lc.Content<SelectLocationUiState>>(firstState)
+ assertEquals(RelayListType.ENTRY, firstState.value.relayListType)
// Select entry
viewModel.selectRelay(mockRelayItem)
// Assert relay list type is exit
val secondState = awaitItem()
- assertIs<SelectLocationUiState.Data>(secondState)
- assertEquals(RelayListType.EXIT, secondState.relayListType)
+ assertIs<Lc.Content<SelectLocationUiState>>(secondState)
+ assertEquals(RelayListType.EXIT, secondState.value.relayListType)
coVerify { mockWireguardConstraintsRepository.setEntryLocation(relayItemId) }
}
}
diff --git a/android/lib/resource/src/main/res/values-da/strings.xml b/android/lib/resource/src/main/res/values-da/strings.xml
index 3e83bea3e2..a20e6e31f4 100644
--- a/android/lib/resource/src/main/res/values-da/strings.xml
+++ b/android/lib/resource/src/main/res/values-da/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Søg</string>
<string name="search_location_empty_text">Intet resultat for \"%1$s\". Prøv en anden søgning</string>
<string name="search_placeholder">Søg efter...</string>
- <string name="search_query_empty">Indtast mindst 2 tegn for at starte søgningen.</string>
<string name="select_location">Vælg placering</string>
<string name="send">Send</string>
<string name="send_anyway">Send alligevel</string>
diff --git a/android/lib/resource/src/main/res/values-de/strings.xml b/android/lib/resource/src/main/res/values-de/strings.xml
index ec79fef641..a1776ad4aa 100644
--- a/android/lib/resource/src/main/res/values-de/strings.xml
+++ b/android/lib/resource/src/main/res/values-de/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Suche</string>
<string name="search_location_empty_text">Kein Ergebnis für „%1$s“, bitte versuchen Sie einen anderen Suchbegriff</string>
<string name="search_placeholder">Suchen nach …</string>
- <string name="search_query_empty">Geben Sie mindestens 2 Zeichen ein, um die Suche zu starten.</string>
<string name="select_location">Ort auswählen</string>
<string name="send">Senden</string>
<string name="send_anyway">Trotzdem senden</string>
diff --git a/android/lib/resource/src/main/res/values-es/strings.xml b/android/lib/resource/src/main/res/values-es/strings.xml
index 1bc12accd3..676c9d1f11 100644
--- a/android/lib/resource/src/main/res/values-es/strings.xml
+++ b/android/lib/resource/src/main/res/values-es/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Buscar</string>
<string name="search_location_empty_text">No hay resultados para «%1$s», intente una búsqueda diferente</string>
<string name="search_placeholder">Buscar...</string>
- <string name="search_query_empty">Escriba al menos 2 caracteres para iniciar la búsqueda.</string>
<string name="select_location">Seleccionar ubicación</string>
<string name="send">Enviar</string>
<string name="send_anyway">Enviar de todos modos</string>
diff --git a/android/lib/resource/src/main/res/values-fi/strings.xml b/android/lib/resource/src/main/res/values-fi/strings.xml
index 74ea35fcec..90e5fd8a25 100644
--- a/android/lib/resource/src/main/res/values-fi/strings.xml
+++ b/android/lib/resource/src/main/res/values-fi/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Haku</string>
<string name="search_location_empty_text">Haulle \"%1$s\" ei löytynyt tuloksia. Kokeile toista hakua.</string>
<string name="search_placeholder">Hae...</string>
- <string name="search_query_empty">Aloita haku kirjoittamalla vähintään 2 merkkiä.</string>
<string name="select_location">Valitse sijainti</string>
<string name="send">Lähetä</string>
<string name="send_anyway">Lähetä silti</string>
diff --git a/android/lib/resource/src/main/res/values-fr/strings.xml b/android/lib/resource/src/main/res/values-fr/strings.xml
index 05b42d99d5..d1c8c11364 100644
--- a/android/lib/resource/src/main/res/values-fr/strings.xml
+++ b/android/lib/resource/src/main/res/values-fr/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Recherche</string>
<string name="search_location_empty_text">Aucun résultat pour « %1$s ». Veuillez essayer une autre recherche</string>
<string name="search_placeholder">Rechercher...</string>
- <string name="search_query_empty">Saisissez au moins 2 caractères pour commencer la recherche.</string>
<string name="select_location">Sélectionner une localisation</string>
<string name="send">Envoyer</string>
<string name="send_anyway">Envoyer quand même</string>
diff --git a/android/lib/resource/src/main/res/values-it/strings.xml b/android/lib/resource/src/main/res/values-it/strings.xml
index 89109175f8..053ca1bbd3 100644
--- a/android/lib/resource/src/main/res/values-it/strings.xml
+++ b/android/lib/resource/src/main/res/values-it/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Cerca</string>
<string name="search_location_empty_text">Nessun risultato per \"%1$s\", prova una ricerca diversa</string>
<string name="search_placeholder">Cerca...</string>
- <string name="search_query_empty">Digita almeno 2 caratteri per iniziare la ricerca.</string>
<string name="select_location">Seleziona posizione</string>
<string name="send">Invia</string>
<string name="send_anyway">Invia comunque</string>
diff --git a/android/lib/resource/src/main/res/values-ja/strings.xml b/android/lib/resource/src/main/res/values-ja/strings.xml
index de93bef8c2..5936212e78 100644
--- a/android/lib/resource/src/main/res/values-ja/strings.xml
+++ b/android/lib/resource/src/main/res/values-ja/strings.xml
@@ -305,7 +305,6 @@
<string name="search">検索</string>
<string name="search_location_empty_text">「%1$s」の結果はありません。別の検索をお試しください。</string>
<string name="search_placeholder">検索...</string>
- <string name="search_query_empty">検索を開始するには2文字以上を入力してください。</string>
<string name="select_location">場所を選択する</string>
<string name="send">送信</string>
<string name="send_anyway">とにかく送信する</string>
diff --git a/android/lib/resource/src/main/res/values-ko/strings.xml b/android/lib/resource/src/main/res/values-ko/strings.xml
index 0237af5df8..2c3c265ca0 100644
--- a/android/lib/resource/src/main/res/values-ko/strings.xml
+++ b/android/lib/resource/src/main/res/values-ko/strings.xml
@@ -305,7 +305,6 @@
<string name="search">검색</string>
<string name="search_location_empty_text">\"%1$s\" 검색 결과가 없습니다. 다른 검색어를 시도하세요</string>
<string name="search_placeholder">검색...</string>
- <string name="search_query_empty">검색을 시작하려면 2자 이상 입력하세요.</string>
<string name="select_location">위치 선택</string>
<string name="send">전송</string>
<string name="send_anyway">그래도 전송</string>
diff --git a/android/lib/resource/src/main/res/values-my/strings.xml b/android/lib/resource/src/main/res/values-my/strings.xml
index 3fece6dbff..c9b4d7408e 100644
--- a/android/lib/resource/src/main/res/values-my/strings.xml
+++ b/android/lib/resource/src/main/res/values-my/strings.xml
@@ -305,7 +305,6 @@
<string name="search">ရှာဖွေမှု</string>
<string name="search_location_empty_text">\"%1$s\" အတွက် ရလဒ်မရှိပါ၊ အခြားရှာဖွေမှုတစ်ခုကို စမ်းလုပ်ကြည့်ပါ</string>
<string name="search_placeholder">ရှာရန်...</string>
- <string name="search_query_empty">စတင်ရှာဖွေရန် အနည်းဆုံး အက္ခရာ 2 လုံး ရိုက်ထည့်ပါ။</string>
<string name="select_location">တည်နေရာ ရွေးရန်</string>
<string name="send">ပို့ရန်</string>
<string name="send_anyway">မည်သို့ပင်ဖြစ်စေ ပို့ရန်</string>
diff --git a/android/lib/resource/src/main/res/values-nb/strings.xml b/android/lib/resource/src/main/res/values-nb/strings.xml
index 8278650ce4..ca5e371606 100644
--- a/android/lib/resource/src/main/res/values-nb/strings.xml
+++ b/android/lib/resource/src/main/res/values-nb/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Søk</string>
<string name="search_location_empty_text">Ingen resultater for «%1$s». Prøv et annet søk</string>
<string name="search_placeholder">Søk etter ...</string>
- <string name="search_query_empty">Skriv minst 2 tegn for å begynne å søke.</string>
<string name="select_location">Velg plassering</string>
<string name="send">Send</string>
<string name="send_anyway">Send allikevel</string>
diff --git a/android/lib/resource/src/main/res/values-nl/strings.xml b/android/lib/resource/src/main/res/values-nl/strings.xml
index 31dde41616..e78a6ae581 100644
--- a/android/lib/resource/src/main/res/values-nl/strings.xml
+++ b/android/lib/resource/src/main/res/values-nl/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Zoeken</string>
<string name="search_location_empty_text">Geen resultaat voor \"%1$s\", probeer een andere zoekopdracht</string>
<string name="search_placeholder">Zoeken naar...</string>
- <string name="search_query_empty">Voer minimaal 2 tekens in om te beginnen met zoeken.</string>
<string name="select_location">Locatie selecteren</string>
<string name="send">Verzenden</string>
<string name="send_anyway">Toch verzenden</string>
diff --git a/android/lib/resource/src/main/res/values-pl/strings.xml b/android/lib/resource/src/main/res/values-pl/strings.xml
index 9db21876a5..91f41e2d4a 100644
--- a/android/lib/resource/src/main/res/values-pl/strings.xml
+++ b/android/lib/resource/src/main/res/values-pl/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Wyszukaj</string>
<string name="search_location_empty_text">Brak wyników dla hasła „%1$s”, spróbuj innego wyszukiwania</string>
<string name="search_placeholder">Wyszukaj...</string>
- <string name="search_query_empty">Wpisz co najmniej 2 znaki, aby rozpocząć wyszukiwanie.</string>
<string name="select_location">Wybierz lokalizację</string>
<string name="send">Wyślij</string>
<string name="send_anyway">Mimo to wyślij</string>
diff --git a/android/lib/resource/src/main/res/values-pt/strings.xml b/android/lib/resource/src/main/res/values-pt/strings.xml
index a07690cd88..d8f226c99c 100644
--- a/android/lib/resource/src/main/res/values-pt/strings.xml
+++ b/android/lib/resource/src/main/res/values-pt/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Pesquisar</string>
<string name="search_location_empty_text">Nenhum resultado para \"%1$s\", tente uma pesquisa diferente</string>
<string name="search_placeholder">Pesquisar por...</string>
- <string name="search_query_empty">Digite pelo menos 2 caracteres para iniciar a pesquisa.</string>
<string name="select_location">Selecionar localização</string>
<string name="send">Enviar</string>
<string name="send_anyway">Enviar mesmo assim</string>
diff --git a/android/lib/resource/src/main/res/values-ru/strings.xml b/android/lib/resource/src/main/res/values-ru/strings.xml
index fee90d7ca7..b0ea223ac7 100644
--- a/android/lib/resource/src/main/res/values-ru/strings.xml
+++ b/android/lib/resource/src/main/res/values-ru/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Поиск</string>
<string name="search_location_empty_text">По запросу «%1$s» ничего не найдено — попробуйте другой запрос</string>
<string name="search_placeholder">Поиск...</string>
- <string name="search_query_empty">Чтобы начать поиск, введите не менее двух символов.</string>
<string name="select_location">Выбор местоположения</string>
<string name="send">Отправить</string>
<string name="send_anyway">Все равно отправить</string>
diff --git a/android/lib/resource/src/main/res/values-sv/strings.xml b/android/lib/resource/src/main/res/values-sv/strings.xml
index d93fc363c5..1300f16f88 100644
--- a/android/lib/resource/src/main/res/values-sv/strings.xml
+++ b/android/lib/resource/src/main/res/values-sv/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Sök</string>
<string name="search_location_empty_text">Inga resultat hittades för \"%1$s\", försök med en annan sökning</string>
<string name="search_placeholder">Sök efter …</string>
- <string name="search_query_empty">Ange minst två tecken för att börja söka.</string>
<string name="select_location">Välj plats</string>
<string name="send">Skicka</string>
<string name="send_anyway">Skicka ändå</string>
diff --git a/android/lib/resource/src/main/res/values-th/strings.xml b/android/lib/resource/src/main/res/values-th/strings.xml
index 0eeee4df5f..6512a46c1f 100644
--- a/android/lib/resource/src/main/res/values-th/strings.xml
+++ b/android/lib/resource/src/main/res/values-th/strings.xml
@@ -305,7 +305,6 @@
<string name="search">ค้นหา</string>
<string name="search_location_empty_text">ไม่พบผลลัพธ์สำหรับ \"%1$s\" โปรดลองใช้คำค้นหาอื่น</string>
<string name="search_placeholder">ค้นหา…</string>
- <string name="search_query_empty">พิมพ์อย่างน้อย 2 ตัวอักษร เพื่อเริ่มการค้นหา</string>
<string name="select_location">เลือกตำแหน่งที่ตั้ง</string>
<string name="send">ส่ง</string>
<string name="send_anyway">ส่งต่อไป</string>
diff --git a/android/lib/resource/src/main/res/values-tr/strings.xml b/android/lib/resource/src/main/res/values-tr/strings.xml
index 30a44f01de..d9ea10ef57 100644
--- a/android/lib/resource/src/main/res/values-tr/strings.xml
+++ b/android/lib/resource/src/main/res/values-tr/strings.xml
@@ -305,7 +305,6 @@
<string name="search">Ara</string>
<string name="search_location_empty_text">\"%1$s\" için sonuç yok, lütfen farklı bir arama yapın</string>
<string name="search_placeholder">Ara...</string>
- <string name="search_query_empty">Arama yapmak için en az 2 karakter girin.</string>
<string name="select_location">Konum seçin</string>
<string name="send">Gönder</string>
<string name="send_anyway">Yine de gönder</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
index 0ed3d053c7..ada8779910 100644
--- a/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
+++ b/android/lib/resource/src/main/res/values-zh-rCN/strings.xml
@@ -305,7 +305,6 @@
<string name="search">搜索</string>
<string name="search_location_empty_text">“%1$s”无结果,请尝试其他搜索</string>
<string name="search_placeholder">搜索…</string>
- <string name="search_query_empty">至少输入 2 个字符以开始搜索。</string>
<string name="select_location">选择位置</string>
<string name="send">发送</string>
<string name="send_anyway">仍然发送</string>
diff --git a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
index 58c0671eb2..5beb978dd5 100644
--- a/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
+++ b/android/lib/resource/src/main/res/values-zh-rTW/strings.xml
@@ -305,7 +305,6 @@
<string name="search">搜尋</string>
<string name="search_location_empty_text">找不到「%1$s」的結果,請使改用其他搜尋條件。</string>
<string name="search_placeholder">搜尋…</string>
- <string name="search_query_empty">至少輸入 2 個字元以開始搜尋。</string>
<string name="select_location">選擇位置</string>
<string name="send">傳送</string>
<string name="send_anyway">仍要傳送</string>
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index d9fe661a21..f96fa9668b 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -389,7 +389,6 @@
<string name="x_exit">%s (Exit)</string>
<string name="filters">Filters:</string>
<string name="removed_provider">%s (removed)</string>
- <string name="search_query_empty">Type at least 2 characters to start searching.</string>
<string name="daita_description_slide_1_first_paragraph">Attention: This increases network traffic and will also negatively affect speed, latency, and battery usage. Use with caution on limited plans.</string>
<string name="daita_description_slide_1_second_paragraph">%1$s (%2$s) hides patterns in your encrypted VPN traffic.</string>
<string name="daita_description_slide_1_third_paragraph">By using sophisticated AI it’s possible to analyze the traffic of data packets going in and out of your device (even if the traffic is encrypted).</string>