summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-08-04 17:04:50 +0200
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2025-08-08 13:04:00 +0200
commitb82153a408f035155c5a26fb68fcaa09e428ca2c (patch)
treeeb8e89e724ea6ca5403c1c38dce4eb846702e823 /android
parent6dee63b6cb3e7dbf9e0d6c77d20a3a6e0dba0de1 (diff)
downloadmullvadvpn-b82153a408f035155c5a26fb68fcaa09e428ca2c.tar.xz
mullvadvpn-b82153a408f035155c5a26fb68fcaa09e428ca2c.zip
Replace select hop code with use cases
Also split the select hop code into select hop and modify multihop Refactor relay list type
Diffstat (limited to 'android')
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt11
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt37
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsListUiStatePreviewParameterProvider.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SelectLocationsUiStatePreviewParameterProvider.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt13
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt37
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt162
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListType.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SelectLocationListUiState.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt16
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ModifyMultihopUseCase.kt104
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt9
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectHopUseCase.kt35
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt105
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModel.kt23
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt136
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectRelay.kt32
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt48
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt20
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt11
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt6
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt39
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml3
-rw-r--r--android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt6
36 files changed, 693 insertions, 311 deletions
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 fa41fa8ff5..46fa1b0e58 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
@@ -12,9 +12,9 @@ import io.mockk.verify
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS
import net.mullvad.mullvadvpn.compose.setContentWithTheme
+import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
-import net.mullvad.mullvadvpn.lib.model.Hop
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
@@ -41,7 +41,7 @@ class SearchLocationScreenTest {
private fun ComposeContext.initScreen(
state: Lce<Unit, SearchLocationUiState, Unit>,
- onSelectHop: (Hop) -> Unit = {},
+ onSelectRelayItem: (RelayItem, RelayListType) -> Unit = { _, _ -> },
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit = { _, _, _ -> },
onSearchInputChanged: (String) -> Unit = {},
onCreateCustomList: (location: RelayItem.Location?) -> Unit = {},
@@ -63,7 +63,7 @@ class SearchLocationScreenTest {
setContentWithTheme {
SearchLocationScreen(
state = state,
- onSelectHop = onSelectHop,
+ onSelectRelayItem = onSelectRelayItem,
onToggleExpand = onToggleExpand,
onSearchInputChanged = onSearchInputChanged,
onCreateCustomList = onCreateCustomList,
@@ -89,9 +89,10 @@ class SearchLocationScreenTest {
Lce.Content(
SearchLocationUiState(
searchTerm = "",
+ relayListType = RelayListType.Single,
filterChips = emptyList(),
relayListItems = emptyList(),
- emptyList(),
+ customLists = emptyList(),
)
),
onSearchInputChanged = mockedSearchTermInput,
@@ -115,6 +116,7 @@ class SearchLocationScreenTest {
Lce.Content(
SearchLocationUiState(
searchTerm = mockSearchString,
+ relayListType = RelayListType.Single,
filterChips = emptyList(),
relayListItems =
listOf(RelayListItem.LocationsEmptyText(mockSearchString)),
@@ -138,6 +140,7 @@ class SearchLocationScreenTest {
Lce.Content(
SearchLocationUiState(
searchTerm = mockSearchString,
+ relayListType = RelayListType.Single,
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 35faa62190..5fdf9c047c 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
@@ -15,6 +15,7 @@ import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES
import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_ITEM_CUSTOM_LISTS
import net.mullvad.mullvadvpn.compose.setContentWithTheme
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
@@ -57,7 +58,8 @@ class SelectLocationScreenTest {
private fun ComposeContext.initScreen(
state: Lc<Unit, SelectLocationUiState> = Lc.Loading(Unit),
- onSelectHop: (hop: Hop, relayListType: RelayListType) -> Unit = { _, _ -> },
+ onSelectHop: (hop: Hop) -> Unit = {},
+ onModifyMultihop: (RelayItem, MultihopRelayListType) -> Unit = { _, _ -> },
onSearchClick: (RelayListType) -> Unit = {},
onBackClick: () -> Unit = {},
onFilterClick: () -> Unit = {},
@@ -76,7 +78,7 @@ class SelectLocationScreenTest {
onEditCustomListName: (RelayItem.CustomList) -> Unit = {},
onEditLocationsCustomList: (RelayItem.CustomList) -> Unit = {},
onDeleteCustomList: (RelayItem.CustomList) -> Unit = {},
- onSelectRelayList: (RelayListType) -> Unit = {},
+ onSelectRelayList: (MultihopRelayListType) -> Unit = {},
openDaitaSettings: () -> Unit = {},
onRecentsToggleEnableClick: () -> Unit = {},
) {
@@ -85,6 +87,7 @@ class SelectLocationScreenTest {
SelectLocationScreen(
state = state,
onSelectHop = onSelectHop,
+ onModifyMultihop = onModifyMultihop,
onSearchClick = onSearchClick,
onBackClick = onBackClick,
onFilterClick = onFilterClick,
@@ -112,6 +115,7 @@ class SelectLocationScreenTest {
MutableStateFlow(
Lce.Content(
SelectLocationListUiState(
+ relayListType = RelayListType.Single,
relayListItems =
DUMMY_RELAY_COUNTRIES.map {
RelayListItem.GeoLocationItem(
@@ -129,7 +133,7 @@ class SelectLocationScreenTest {
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -156,6 +160,7 @@ class SelectLocationScreenTest {
SelectLocationListUiState(
relayListItems = listOf(RelayListItem.CustomListFooter(false)),
customLists = emptyList(),
+ relayListType = RelayListType.Single,
)
)
)
@@ -165,7 +170,7 @@ class SelectLocationScreenTest {
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -188,17 +193,18 @@ class SelectLocationScreenTest {
SelectLocationListUiState(
relayListItems = listOf(RelayListItem.CustomListItem(customList)),
customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ relayListType = RelayListType.Single,
)
)
)
- val mockedOnSelectHop: (Hop, RelayListType) -> Unit = mockk(relaxed = true)
+ val mockedOnSelectHop: (Hop) -> Unit = mockk(relaxed = true)
initScreen(
state =
Lc.Content(
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -211,7 +217,7 @@ class SelectLocationScreenTest {
onNodeWithText(customList.relay.name).performClick()
// Assert
- verify { mockedOnSelectHop(customList, RelayListType.EXIT) }
+ verify { mockedOnSelectHop(customList) }
}
@Test
@@ -225,17 +231,18 @@ class SelectLocationScreenTest {
SelectLocationListUiState(
relayListItems = listOf(RelayListItem.RecentListItem(recent)),
customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ relayListType = RelayListType.Single,
)
)
)
- val mockedOnSelectHop: (Hop, RelayListType) -> Unit = mockk(relaxed = true)
+ val mockedOnSelectHop: (Hop) -> Unit = mockk(relaxed = true)
initScreen(
state =
Lc.Content(
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -248,7 +255,7 @@ class SelectLocationScreenTest {
onNodeWithText(recent.relay.name).performClick()
// Assert
- verify { mockedOnSelectHop(recent, RelayListType.EXIT) }
+ verify { mockedOnSelectHop(recent) }
}
@Test
@@ -262,17 +269,18 @@ class SelectLocationScreenTest {
SelectLocationListUiState(
relayListItems = listOf(RelayListItem.CustomListItem(hop = customList)),
customLists = DUMMY_RELAY_ITEM_CUSTOM_LISTS,
+ relayListType = RelayListType.Single,
)
)
)
- val mockedOnSelectHop: (Hop, RelayListType) -> Unit = mockk(relaxed = true)
+ val mockedOnSelectHop: (Hop) -> Unit = mockk(relaxed = true)
initScreen(
state =
Lc.Content(
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -305,17 +313,18 @@ class SelectLocationScreenTest {
)
),
customLists = emptyList(),
+ relayListType = RelayListType.Single,
)
)
)
- val mockedOnSelectHop: (Hop, RelayListType) -> Unit = mockk(relaxed = true)
+ val mockedOnSelectHop: (Hop) -> Unit = mockk(relaxed = true)
initScreen(
state =
Lc.Content(
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsListUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsListUiStatePreviewParameterProvider.kt
index 38f4adc250..5f09c46b78 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsListUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsListUiStatePreviewParameterProvider.kt
@@ -1,6 +1,8 @@
package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
+import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItemPreviewData
import net.mullvad.mullvadvpn.util.Lce
@@ -17,6 +19,7 @@ class SearchLocationsListUiStatePreviewParameterProvider :
isSearching = false,
),
customLists = emptyList(),
+ relayListType = RelayListType.Multihop(MultihopRelayListType.EXIT),
)
),
Lce.Loading(Unit),
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 c868360c55..ea560d4ae2 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
@@ -1,6 +1,8 @@
package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
+import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItemPreviewData
import net.mullvad.mullvadvpn.usecase.FilterChip
@@ -21,6 +23,7 @@ class SearchLocationsUiStatePreviewParameterProvider :
isSearching = true,
),
customLists = emptyList(),
+ relayListType = RelayListType.Multihop(MultihopRelayListType.ENTRY),
)
),
Lce.Error(Unit),
@@ -31,6 +34,7 @@ class SearchLocationsUiStatePreviewParameterProvider :
relayListItems =
RelayListItemPreviewData.generateEmptyList("Mullvad", isSearching = true),
customLists = emptyList(),
+ relayListType = RelayListType.Multihop(MultihopRelayListType.ENTRY),
)
),
Lce.Content(
@@ -43,6 +47,7 @@ class SearchLocationsUiStatePreviewParameterProvider :
isSearching = true,
),
customLists = emptyList(),
+ relayListType = RelayListType.Multihop(MultihopRelayListType.ENTRY),
)
),
)
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 34275cf241..a130112307 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
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.usecase.FilterChip
@@ -16,7 +17,7 @@ class SelectLocationsUiStatePreviewParameterProvider :
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -29,7 +30,7 @@ class SelectLocationsUiStatePreviewParameterProvider :
FilterChip.Provider(PROVIDER_COUNT),
),
multihopEnabled = false,
- relayListType = RelayListType.EXIT,
+ relayListType = RelayListType.Single,
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -38,7 +39,7 @@ class SelectLocationsUiStatePreviewParameterProvider :
SelectLocationUiState(
filterChips = emptyList(),
multihopEnabled = true,
- relayListType = RelayListType.ENTRY,
+ relayListType = RelayListType.Multihop(MultihopRelayListType.ENTRY),
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = true,
@@ -51,7 +52,7 @@ class SelectLocationsUiStatePreviewParameterProvider :
FilterChip.Provider(PROVIDER_COUNT),
),
multihopEnabled = true,
- relayListType = RelayListType.ENTRY,
+ relayListType = RelayListType.Multihop(MultihopRelayListType.ENTRY),
isSearchButtonEnabled = true,
isFilterButtonEnabled = true,
isRecentsEnabled = 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 95611ccf5e..114bc1a030 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
@@ -3,6 +3,7 @@ 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@@ -172,7 +173,7 @@ fun CustomListLocationsScreen(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar),
)
.padding(horizontal = Dimens.mediumPadding)
- .fillMaxWidth(),
+ .fillMaxSize(),
state = lazyListState,
) {
when (state.content) {
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 98b553cac1..3c3402d1c9 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
@@ -42,6 +42,7 @@ fun LazyListScope.relayListContent(
relayListItems: List<RelayListItem>,
customLists: List<RelayItem.CustomList>,
onSelectHop: (Hop) -> Unit,
+ onSelectRelayItem: (RelayItem) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
customListHeader: @Composable (LazyItemScope.() -> Unit) = {},
@@ -58,14 +59,14 @@ fun LazyListScope.relayListContent(
is RelayListItem.CustomListItem ->
CustomListItem(
listItem,
- onSelectHop = onSelectHop,
+ onSelectHop = { onSelectRelayItem(it.exit()) },
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
)
is RelayListItem.CustomListEntryItem ->
CustomListEntryItem(
listItem,
- onSelectHop = onSelectHop,
+ onSelectHop = { onSelectRelayItem(it.exit()) },
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
)
@@ -74,7 +75,7 @@ fun LazyListScope.relayListContent(
is RelayListItem.GeoLocationItem ->
GeoLocationItem(
listItem,
- onSelectHop = onSelectHop,
+ onSelectHop = { onSelectRelayItem(it.exit()) },
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
customLists = customLists,
@@ -110,7 +111,7 @@ fun Modifier.positionalPadding(itemPosition: ItemPosition): Modifier =
@Composable
private fun GeoLocationItem(
listItem: RelayListItem.GeoLocationItem,
- onSelectHop: (Hop) -> Unit,
+ onSelectHop: (Hop.Single<*>) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
customLists: List<RelayItem.CustomList>,
@@ -150,7 +151,7 @@ private fun RecentListItem(
@Composable
private fun CustomListItem(
listItem: RelayListItem.CustomListItem,
- onSelectHop: (Hop) -> Unit,
+ onSelectHop: (Hop.Single<*>) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
@@ -166,7 +167,7 @@ private fun CustomListItem(
@Composable
private fun CustomListEntryItem(
listItem: RelayListItem.CustomListEntryItem,
- onSelectHop: (Hop) -> Unit,
+ onSelectHop: (Hop.Single<*>) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
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 f3893dcd40..a14788584d 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
@@ -67,13 +67,11 @@ import net.mullvad.mullvadvpn.compose.transitions.TopLevelTransition
import net.mullvad.mullvadvpn.compose.util.CollectSideEffectWithLifecycle
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.model.CustomListId
-import net.mullvad.mullvadvpn.lib.model.Hop
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
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.component.relaylist.displayName
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.location.SearchLocationSideEffect
@@ -90,7 +88,7 @@ private fun PreviewSearchLocationScreen(
SearchLocationScreen(
state = state,
snackbarHostState = SnackbarHostState(),
- onSelectHop = {},
+ onSelectRelayItem = { _, _ -> },
onToggleExpand = { _, _, _ -> },
onSearchInputChanged = {},
onCreateCustomList = {},
@@ -150,17 +148,33 @@ fun SearchLocation(
message = context.getString(R.string.error_occurred)
)
}
-
- is SearchLocationSideEffect.HopInactive -> {
+ is SearchLocationSideEffect.EntryAlreadySelected ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(
+ R.string.relay_item_already_selected_as_entry,
+ it.relayItem.name,
+ )
+ )
+ }
+ is SearchLocationSideEffect.ExitAlreadySelected ->
launch {
snackbarHostState.showSnackbarImmediately(
message =
context.getString(
- R.string.relayitem_is_inactive,
- it.hop.displayName(context),
+ R.string.relay_item_already_selected_as_exit,
+ it.relayItem.name,
)
)
}
+ is SearchLocationSideEffect.RelayItemInactive -> {
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(R.string.relayitem_is_inactive, it.relayItem.name)
+ )
+ }
}
}
}
@@ -188,7 +202,7 @@ fun SearchLocation(
SearchLocationScreen(
state = state,
snackbarHostState = snackbarHostState,
- onSelectHop = viewModel::selectHop,
+ onSelectRelayItem = viewModel::selectRelayItem,
onToggleExpand = viewModel::onToggleExpand,
onSearchInputChanged = viewModel::onSearchInputUpdated,
onCreateCustomList =
@@ -233,7 +247,7 @@ fun SearchLocation(
fun SearchLocationScreen(
state: Lce<Unit, SearchLocationUiState, Unit>,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
- onSelectHop: (Hop) -> Unit,
+ onSelectRelayItem: (RelayItem, RelayListType) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onSearchInputChanged: (String) -> Unit,
onCreateCustomList: (location: RelayItem.Location?) -> Unit,
@@ -313,7 +327,10 @@ fun SearchLocationScreen(
relayListContent(
relayListItems = state.value.relayListItems,
customLists = state.value.customLists,
- onSelectHop = onSelectHop,
+ onSelectHop = { error("Can not select hop in search screen") },
+ onSelectRelayItem = {
+ onSelectRelayItem(it, state.value.relayListType)
+ },
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = { newSheetState ->
locationBottomSheetState = newSheetState
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 eba2498ca1..b7172865c2 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
@@ -38,6 +38,7 @@ import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.compose.util.RunOnKeyChange
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.Hop
+import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
@@ -61,6 +62,7 @@ private fun PreviewSelectLocationList(
lazyListState = rememberLazyListState(),
openDaitaSettings = {},
onSelectHop = {},
+ onSelectRelayItem = { _, _ -> },
onUpdateBottomSheetState = {},
onAddCustomList = {},
onEditCustomLists = {},
@@ -77,7 +79,8 @@ private typealias Content = Lce.Content<SelectLocationListUiState>
@Composable
fun SelectLocationList(
relayListType: RelayListType,
- onSelectHop: (Hop, RelayListType) -> Unit,
+ onSelectHop: (Hop) -> Unit,
+ onSelectRelayItem: (RelayItem, RelayListType) -> Unit,
openDaitaSettings: () -> Unit,
onAddCustomList: () -> Unit,
onEditCustomLists: (() -> Unit)?,
@@ -85,7 +88,7 @@ fun SelectLocationList(
) {
val viewModel =
koinViewModel<SelectLocationListViewModel>(
- key = relayListType.name,
+ key = relayListType.toString(),
parameters = { parametersOf(relayListType) },
)
val state by viewModel.uiState.collectAsStateWithLifecycle()
@@ -103,7 +106,8 @@ fun SelectLocationList(
state = state,
lazyListState = lazyListState,
openDaitaSettings = openDaitaSettings,
- onSelectHop = { onSelectHop(it, relayListType) },
+ onSelectHop = onSelectHop,
+ onSelectRelayItem = onSelectRelayItem,
onUpdateBottomSheetState = onUpdateBottomSheetState,
onAddCustomList = onAddCustomList,
onEditCustomLists = onEditCustomLists,
@@ -117,6 +121,7 @@ private fun SelectLocationListContent(
lazyListState: LazyListState,
openDaitaSettings: () -> Unit,
onSelectHop: (Hop) -> Unit,
+ onSelectRelayItem: (relayItem: RelayItem, relayListType: RelayListType) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
onAddCustomList: () -> Unit,
onEditCustomLists: (() -> Unit)?,
@@ -159,6 +164,7 @@ private fun SelectLocationListContent(
relayListItems = state.value.relayListItems,
customLists = state.value.customLists,
onSelectHop = onSelectHop,
+ onSelectRelayItem = { onSelectRelayItem(it, state.value.relayListType) },
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
customListHeader = {
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 dab28c681e..3abeee2143 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,6 +73,7 @@ import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicator
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithSmallTopBar
import net.mullvad.mullvadvpn.compose.extensions.dropUnlessResumed
import net.mullvad.mullvadvpn.compose.preview.SelectLocationsUiStatePreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.compose.transitions.TopLevelTransition
@@ -102,7 +103,8 @@ private fun PreviewSelectLocationScreen(
SelectLocationScreen(
state = state,
snackbarHostState = SnackbarHostState(),
- onSelectHop = { _, _ -> },
+ onSelectHop = {},
+ onModifyMultihop = { _, _ -> },
onSearchClick = {},
onBackClick = {},
onFilterClick = {},
@@ -147,6 +149,7 @@ fun SelectLocation(
val snackbarHostState = remember { SnackbarHostState() }
val context = LocalContext.current
+ val focusManager = LocalFocusManager.current
CollectSideEffectWithLifecycle(vm.uiSideEffect) {
when (it) {
SelectLocationSideEffect.CloseScreen -> backNavigator.navigateBack(result = true)
@@ -164,7 +167,26 @@ fun SelectLocation(
message = context.getString(R.string.error_occurred)
)
}
-
+ is SelectLocationSideEffect.EntryAlreadySelected ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(
+ R.string.relay_item_already_selected_as_entry,
+ it.relayItem.name,
+ )
+ )
+ }
+ is SelectLocationSideEffect.ExitAlreadySelected ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(
+ R.string.relay_item_already_selected_as_exit,
+ it.relayItem.name,
+ )
+ )
+ }
is SelectLocationSideEffect.RelayItemInactive ->
launch {
snackbarHostState.showSnackbarImmediately(
@@ -175,6 +197,24 @@ fun SelectLocation(
)
)
}
+ SelectLocationSideEffect.EntryAndExitAreSame ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message = context.getString(R.string.entry_and_exit_are_same)
+ )
+ }
+ is SelectLocationSideEffect.FocusExitList ->
+ launch {
+ // If multihop is enabled and the user selects a location or custom list in the
+ // entry list
+ // the app will switch to the exit list. Normally in this case the focus will
+ // stay in the
+ // entry list, but in this case we want move the focus to the exit list.
+ focusManager.moveFocus(FocusDirection.Right)
+ if (it.relayItem.hasChildren) {
+ focusManager.moveFocus(FocusDirection.Right)
+ }
+ }
}
}
@@ -197,10 +237,12 @@ fun SelectLocation(
searchSelectedLocationResultRecipient.onResult { result ->
when (result) {
- RelayListType.ENTRY -> {
- vm.selectRelayList(RelayListType.EXIT)
- }
- RelayListType.EXIT -> backNavigator.navigateBack(result = true)
+ RelayListType.Single -> backNavigator.navigateBack(result = true)
+ is RelayListType.Multihop ->
+ when (result.multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> vm.selectRelayList(MultihopRelayListType.EXIT)
+ MultihopRelayListType.EXIT -> backNavigator.navigateBack(result = true)
+ }
}
}
@@ -208,6 +250,7 @@ fun SelectLocation(
state = state.value,
snackbarHostState = snackbarHostState,
onSelectHop = vm::selectHop,
+ onModifyMultihop = vm::modifyMultihop,
onSearchClick = { navigator.navigate(SearchLocationDestination(it)) },
onBackClick = dropUnlessResumed { backNavigator.navigateBack() },
onFilterClick = dropUnlessResumed { navigator.navigate(FilterDestination) },
@@ -256,7 +299,8 @@ fun SelectLocation(
fun SelectLocationScreen(
state: Lc<Unit, SelectLocationUiState>,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
- onSelectHop: (item: Hop, relayListType: RelayListType) -> Unit,
+ onSelectHop: (item: Hop) -> Unit,
+ onModifyMultihop: (relayItem: RelayItem, relayListType: MultihopRelayListType) -> Unit,
onSearchClick: (RelayListType) -> Unit,
onBackClick: () -> Unit,
onFilterClick: () -> Unit,
@@ -270,7 +314,7 @@ fun SelectLocationScreen(
onEditCustomListName: (RelayItem.CustomList) -> Unit,
onEditLocationsCustomList: (RelayItem.CustomList) -> Unit,
onDeleteCustomList: (RelayItem.CustomList) -> Unit,
- onSelectRelayList: (RelayListType) -> Unit,
+ onSelectRelayList: (MultihopRelayListType) -> Unit,
openDaitaSettings: () -> Unit,
) {
val backgroundColor = MaterialTheme.colorScheme.surface
@@ -349,18 +393,16 @@ fun SelectLocationScreen(
is Lc.Content -> {
val pagerState =
rememberPagerState(
- initialPage = state.value.relayListType.ordinal,
- pageCount = {
- if (state.value.multihopEnabled) {
- RelayListType.entries.size
- } else {
- 1
- }
- },
+ initialPage = state.value.relayListType.initialPage(),
+ pageCount = { state.value.relayListType.pageCount() },
)
- if (state.value.multihopEnabled) {
- MultihopBar(pagerState, state.value.relayListType, onSelectRelayList)
+ if (state.value.relayListType is RelayListType.Multihop) {
+ MultihopBar(
+ pagerState,
+ state.value.relayListType.multihopRelayListType,
+ onSelectRelayList,
+ )
}
AnimatedContent(
@@ -385,6 +427,7 @@ fun SelectLocationScreen(
pagerState,
state = state.value,
onSelectHop = onSelectHop,
+ onModifyMultihop = onModifyMultihop,
openDaitaSettings = openDaitaSettings,
onAddCustomList = { onCreateCustomList(null) },
onEditCustomLists = onEditCustomLists,
@@ -464,8 +507,8 @@ private fun SelectLocationDropdownMenu(
@Composable
private fun MultihopBar(
pagerState: PagerState,
- relayListType: RelayListType,
- onSelectHopList: (RelayListType) -> Unit,
+ relayListType: MultihopRelayListType,
+ onSelectHopList: (MultihopRelayListType) -> Unit,
) {
SingleChoiceSegmentedButtonRow(
modifier =
@@ -477,21 +520,21 @@ private fun MultihopBar(
)
) {
MullvadSegmentedStartButton(
- selected = relayListType == RelayListType.ENTRY,
+ selected = relayListType == MultihopRelayListType.ENTRY,
selectedProgress =
1f -
- abs(pagerState.getOffsetDistanceInPages(RelayListType.ENTRY.ordinal))
+ abs(pagerState.getOffsetDistanceInPages(MultihopRelayListType.ENTRY.ordinal))
.coerceIn(0f..1f),
- onClick = { onSelectHopList(RelayListType.ENTRY) },
+ onClick = { onSelectHopList(MultihopRelayListType.ENTRY) },
text = stringResource(id = R.string.entry),
)
MullvadSegmentedEndButton(
- selected = relayListType == RelayListType.EXIT,
+ selected = relayListType == MultihopRelayListType.EXIT,
selectedProgress =
1f -
- abs(pagerState.getOffsetDistanceInPages(RelayListType.EXIT.ordinal))
+ abs(pagerState.getOffsetDistanceInPages(MultihopRelayListType.EXIT.ordinal))
.coerceIn(0f..1f),
- onClick = { onSelectHopList(RelayListType.EXIT) },
+ onClick = { onSelectHopList(MultihopRelayListType.EXIT) },
text = stringResource(id = R.string.exit),
)
}
@@ -502,43 +545,35 @@ private fun MultihopBar(
private fun RelayLists(
pagerState: PagerState,
state: SelectLocationUiState,
- onSelectHop: (Hop, RelayListType) -> Unit,
+ onSelectHop: (hop: Hop) -> Unit,
+ onModifyMultihop: (RelayItem, MultihopRelayListType) -> Unit,
openDaitaSettings: () -> Unit,
onAddCustomList: () -> Unit,
onEditCustomLists: (() -> Unit)?,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
- onSelectRelayList: (RelayListType) -> Unit,
+ onSelectRelayList: (MultihopRelayListType) -> Unit,
) {
- // This is so that when the pager is scrolled by the user the relay list type is updated
- // correctly.
- // If multihop is not enabled, the pager will only have one page, so this will not be called.
- if (state.multihopEnabled) {
+ if (state.relayListType is RelayListType.Multihop) {
+ // This is so that when the pager is scrolled by the user the relay list type is updated
+ // correctly.
+ // If multihop is not enabled, the pager will only have one page, so this will not be
+ // called.
LaunchedEffect(pagerState.currentPage) {
- onSelectRelayList(RelayListType.entries[pagerState.currentPage])
+ onSelectRelayList(MultihopRelayListType.entries[pagerState.currentPage])
+ }
+ // This is so that when the relay list entry or exit button is clicked, the pager will
+ // scroll to the correct page.
+ LaunchedEffect(state.relayListType.multihopRelayListType) {
+ val index = state.relayListType.multihopRelayListType.ordinal
+ pagerState.animateScrollToPage(index)
}
}
- LaunchedEffect(state.relayListType) {
- val index = state.relayListType.ordinal
- pagerState.animateScrollToPage(index)
- }
-
- val focusManager = LocalFocusManager.current
- val onSelectHopInner: (Hop, RelayListType) -> Unit = { hop, relayListType ->
- onSelectHop(hop, relayListType)
- // If multihop is enabled and the user selects a location or custom list in the entry list
- // the app will switch to the exit list. Normally in this case the focus will stay in the
- // entry list, but in this case we want move the focus to the exit list.
- if (
- state.multihopEnabled &&
- relayListType == RelayListType.ENTRY &&
- hop is Hop.Single<*> &&
- hop.isActive
- ) {
- focusManager.moveFocus(FocusDirection.Right)
- if (hop.relay.hasChildren) {
- focusManager.moveFocus(FocusDirection.Right)
- }
+ val onSelectRelayItem: (RelayItem, RelayListType) -> Unit = { relayItem, relayListType ->
+ if (relayListType is RelayListType.Multihop) {
+ onModifyMultihop(relayItem, relayListType.multihopRelayListType)
+ } else {
+ onSelectHop(Hop.Single(relayItem))
}
}
@@ -555,11 +590,12 @@ private fun RelayLists(
SelectLocationList(
relayListType =
if (state.multihopEnabled) {
- RelayListType.entries[pageIndex]
+ RelayListType.Multihop(MultihopRelayListType.entries[pageIndex])
} else {
- RelayListType.EXIT
+ RelayListType.Single
},
- onSelectHop = onSelectHopInner,
+ onSelectHop = onSelectHop,
+ onSelectRelayItem = onSelectRelayItem,
openDaitaSettings = openDaitaSettings,
onAddCustomList = onAddCustomList,
onEditCustomLists = onEditCustomLists,
@@ -572,3 +608,15 @@ private fun RelayLists(
private fun ColumnScope.Loading() {
MullvadCircularProgressIndicatorLarge(modifier = Modifier.align(Alignment.CenterHorizontally))
}
+
+private fun RelayListType.initialPage(): Int =
+ when (this) {
+ is RelayListType.Multihop -> multihopRelayListType.ordinal
+ RelayListType.Single -> 0
+ }
+
+private fun RelayListType.pageCount(): Int =
+ when (this) {
+ is RelayListType.Multihop -> MultihopRelayListType.entries.size
+ RelayListType.Single -> 1
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListType.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListType.kt
index 6640ceea4a..6073992261 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListType.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListType.kt
@@ -1,6 +1,16 @@
package net.mullvad.mullvadvpn.compose.state
-enum class RelayListType {
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+enum class MultihopRelayListType {
ENTRY,
EXIT,
}
+
+sealed interface RelayListType : Parcelable {
+ @Parcelize
+ data class Multihop(val multihopRelayListType: MultihopRelayListType) : RelayListType
+
+ @Parcelize data object Single : RelayListType
+}
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 909e4ea8eb..5ea48d1072 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
@@ -6,6 +6,7 @@ import net.mullvad.mullvadvpn.usecase.FilterChip
data class SearchLocationUiState(
val searchTerm: String,
+ val relayListType: RelayListType,
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 39199b9d04..22b0977074 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
@@ -4,6 +4,7 @@ import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
data class SelectLocationListUiState(
+ val relayListType: RelayListType,
val relayListItems: List<RelayListItem>,
val customLists: List<RelayItem.CustomList>,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index 95e2b4c7e7..e24ad8e9ef 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -38,6 +38,7 @@ import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
import net.mullvad.mullvadvpn.usecase.InternetAvailableUseCase
import net.mullvad.mullvadvpn.usecase.LastKnownLocationUseCase
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopUseCase
import net.mullvad.mullvadvpn.usecase.NewChangelogNotificationUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
import net.mullvad.mullvadvpn.usecase.OutOfTimeUseCase
@@ -45,6 +46,7 @@ import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.usecase.PlayPaymentUseCase
import net.mullvad.mullvadvpn.usecase.ProviderToOwnershipsUseCase
import net.mullvad.mullvadvpn.usecase.RecentsUseCase
+import net.mullvad.mullvadvpn.usecase.SelectHopUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationTitleUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
@@ -160,9 +162,18 @@ val uiModule = module {
single { FilteredRelayListUseCase(get(), get(), get(), get()) }
single { LastKnownLocationUseCase(get()) }
single { SelectedLocationUseCase(get(), get()) }
- single { FilterChipUseCase(get(), get(), get(), get()) }
+ single { FilterChipUseCase(get(), get(), get()) }
single { DeleteCustomDnsUseCase(get()) }
single { RecentsUseCase(get(), get(), get()) }
+ single { SelectHopUseCase(relayListRepository = get()) }
+ single {
+ ModifyMultihopUseCase(
+ relayListRepository = get(),
+ settingsRepository = get(),
+ customListsRepository = get(),
+ wireguardConstraintsRepository = get(),
+ )
+ }
single { InAppNotificationController(get(), get(), get(), get(), get(), MainScope()) }
@@ -223,7 +234,9 @@ val uiModule = module {
viewModel { WireguardCustomPortDialogViewModel(get()) }
viewModel { LoginViewModel(get(), get(), get()) }
viewModel { PrivacyDisclaimerViewModel(get(), IS_PLAY_BUILD) }
- viewModel { SelectLocationViewModel(get(), get(), get(), get(), get(), get(), get()) }
+ viewModel {
+ SelectLocationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get())
+ }
viewModel { SettingsViewModel(get(), get(), get(), get(), IS_PLAY_BUILD) }
viewModel { SplashViewModel(get(), get(), get(), get()) }
viewModel { VoucherDialogViewModel(get()) }
@@ -263,6 +276,7 @@ val uiModule = module {
get(),
get(),
get(),
+ get(),
)
}
viewModel { (relayListType: RelayListType) ->
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 fac0e77321..4e8575f8f9 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
@@ -26,3 +26,6 @@ fun RelayItem.CustomList.canAddLocation(location: RelayItem) =
fun List<RelayItem.CustomList>.getById(id: CustomListId) = this.find { it.id == id }
fun List<CustomList>.getById(id: CustomListId) = this.find { it.id == id }
+
+fun RelayItem.CustomList.onlyContains(relayItem: RelayItem.Location) =
+ this.locations.size == 1 && this.locations.first() == relayItem
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
index 4803b966a9..f493aa97cb 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt
@@ -130,3 +130,25 @@ fun List<RelayItem.Location.Country>.findRelay(
?.find { city -> city.id == geoLocationId.city }
?.relays
?.find { relay -> relay.id == geoLocationId }
+
+/**
+ * Checks if two RelayItems are the same for the purpose of blocking selection. Only relays are
+ * considered the same, cities and countries are not. For the purpose of blocking selection, custom
+ * lists are considered to be a relay if and only if they contain a single relay.
+ */
+fun RelayItem.isTheSameAs(other: RelayItem): Boolean {
+ return when (this) {
+ is RelayItem.Location.Relay -> {
+ (other is RelayItem.Location.Relay && this.id == other.id) ||
+ (other is RelayItem.CustomList && other.onlyContains(this))
+ }
+ is RelayItem.CustomList -> {
+ (other is RelayItem.Location.Relay && this.onlyContains(other)) ||
+ (other is RelayItem.CustomList &&
+ this.locations.size == 1 &&
+ this.locations.first() is RelayItem.Location.Relay &&
+ other.onlyContains(this.locations.first()))
+ }
+ else -> false
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt
index 6b8fd69ff8..06d74eed3f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.repository
import arrow.core.Either
import arrow.core.raise.either
+import arrow.core.raise.ensureNotNull
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -63,10 +64,15 @@ class CustomListsRepository(
* updateCustomList just before this you might get an out of date value.
*/
fun getCustomListById(id: CustomListId): Either<GetCustomListError, CustomList> = either {
- val customLists =
- customLists.value
- ?: raise(GetCustomListError(id)).also { Logger.e("Custom lists never loaded") }
- customLists.firstOrNull { customList -> customList.id == id }
- ?: raise(GetCustomListError(id))
+ val customLists = customLists.value
+ ensureNotNull(customLists) {
+ Logger.e("Custom lists never loaded")
+ GetCustomListError(id)
+ }
+ val foundList = customLists.firstOrNull { customList -> customList.id == id }
+ ensureNotNull(foundList) {
+ Logger.e("Custom list with id $id not found in custom lists")
+ GetCustomListError(id)
+ }
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt
index 8c542202ff..5e7a82d9ed 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCase.kt
@@ -10,7 +10,6 @@ import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.util.shouldFilterByDaita
typealias ModelOwnership = Ownership
@@ -19,7 +18,6 @@ class FilterChipUseCase(
private val relayListFilterRepository: RelayListFilterRepository,
private val providerToOwnershipsUseCase: ProviderToOwnershipsUseCase,
private val settingsRepository: SettingsRepository,
- private val wireguardConstraintsRepository: WireguardConstraintsRepository,
) {
operator fun invoke(relayListType: RelayListType): Flow<List<FilterChip>> =
combine(
@@ -27,19 +25,12 @@ class FilterChipUseCase(
relayListFilterRepository.selectedProviders,
providerToOwnershipsUseCase(),
settingsRepository.settingsUpdates,
- wireguardConstraintsRepository.wireguardConstraints,
- ) {
- selectedOwnership,
- selectedConstraintProviders,
- providerOwnership,
- settings,
- wireguardConstraints ->
+ ) { selectedOwnership, selectedConstraintProviders, providerOwnership, settings ->
filterChips(
selectedOwnership = selectedOwnership,
selectedConstraintProviders = selectedConstraintProviders,
providerToOwnerships = providerOwnership,
daitaDirectOnly = settings?.daitaAndDirectOnly() == true,
- isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
relayListType = relayListType,
)
}
@@ -49,7 +40,6 @@ class FilterChipUseCase(
selectedConstraintProviders: Constraint<Providers>,
providerToOwnerships: Map<ProviderId, Set<Ownership>>,
daitaDirectOnly: Boolean,
- isMultihopEnabled: Boolean,
relayListType: RelayListType,
): List<FilterChip> {
val ownershipFilter = selectedOwnership.getOrNull()
@@ -66,11 +56,7 @@ class FilterChipUseCase(
// If the provider has been removed from the relay list we add it
// so it is visible for the user, because we won't know what
// ownerships it had.
- if (providerOwnerships == null) {
- true
- } else {
- providerOwnerships.contains(ownershipFilter)
- }
+ providerOwnerships?.contains(ownershipFilter) ?: true
}
}
.size
@@ -86,7 +72,6 @@ class FilterChipUseCase(
shouldFilterByDaita(
daitaDirectOnly = daitaDirectOnly,
relayListType = relayListType,
- isMultihopEnabled = isMultihopEnabled,
)
) {
add(FilterChip.Daita)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
index 15eee7e4ed..875d63c1ce 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt
@@ -34,7 +34,6 @@ class FilteredRelayListUseCase(
shouldFilterByDaita =
shouldFilterByDaita(
daitaDirectOnly = settings?.daitaAndDirectOnly() == true,
- isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
relayListType = relayListType,
),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ModifyMultihopUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ModifyMultihopUseCase.kt
new file mode 100644
index 0000000000..de76583166
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/ModifyMultihopUseCase.kt
@@ -0,0 +1,104 @@
+package net.mullvad.mullvadvpn.usecase
+
+import arrow.core.Either
+import arrow.core.left
+import arrow.core.raise.either
+import arrow.core.raise.ensure
+import arrow.core.right
+import co.touchlab.kermit.Logger
+import kotlin.collections.first
+import net.mullvad.mullvadvpn.lib.model.CustomListId
+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.Settings
+import net.mullvad.mullvadvpn.repository.CustomListsRepository
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+import net.mullvad.mullvadvpn.repository.SettingsRepository
+import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
+
+class ModifyMultihopUseCase(
+ private val relayListRepository: RelayListRepository,
+ private val settingsRepository: SettingsRepository,
+ private val customListsRepository: CustomListsRepository,
+ private val wireguardConstraintsRepository: WireguardConstraintsRepository,
+) {
+ suspend operator fun invoke(change: MultihopChange): Either<ModifyMultihopError, Unit> =
+ either {
+ ensure(change.item.active) { ModifyMultihopError.RelayItemInactive(change.item) }
+ val changeId: RelayItemId =
+ change.item.id.convertCustomListWithOnlyHostNameToHostName().bind()
+ val other =
+ when (change) {
+ is MultihopChange.Entry ->
+ settingsRepository.settingsUpdates.value.exit().bind()
+ is MultihopChange.Exit ->
+ settingsRepository.settingsUpdates.value.entry().bind()
+ }
+ .convertCustomListWithOnlyHostNameToHostName()
+ .bind()
+ ensure(!changeId.isSameHost(other)) { ModifyMultihopError.EntrySameAsExit(change.item) }
+ when (change) {
+ is MultihopChange.Entry ->
+ wireguardConstraintsRepository.setEntryLocation(change.item.id)
+ is MultihopChange.Exit ->
+ relayListRepository.updateSelectedRelayLocation(change.item.id)
+ }
+ .mapLeft {
+ Logger.e("Failed to update multihop: $it")
+ ModifyMultihopError.GenericError
+ }
+ .bind()
+ }
+
+ private fun Settings?.exit(): Either<ModifyMultihopError.GenericError, RelayItemId> =
+ this?.relaySettings?.relayConstraints?.location?.getOrNull()?.right()
+ ?: ModifyMultihopError.GenericError.left()
+
+ private fun Settings?.entry(): Either<ModifyMultihopError.GenericError, RelayItemId> =
+ this?.relaySettings
+ ?.relayConstraints
+ ?.wireguardConstraints
+ ?.entryLocation
+ ?.getOrNull()
+ ?.right() ?: ModifyMultihopError.GenericError.left()
+
+ private fun RelayItemId.convertCustomListWithOnlyHostNameToHostName():
+ Either<ModifyMultihopError.GenericError, RelayItemId> =
+ when (this) {
+ is CustomListId ->
+ customListsRepository
+ .getCustomListById(this)
+ .mapLeft {
+ Logger.e("Failed to get custom list by id: $it")
+ ModifyMultihopError.GenericError
+ }
+ .map {
+ if (it.locations.size == 1) {
+ it.locations.first() as? GeoLocationId.Hostname ?: this
+ } else {
+ this
+ }
+ }
+ else -> this.right()
+ }
+
+ private fun RelayItemId.isSameHost(other: RelayItemId): Boolean =
+ this is GeoLocationId.Hostname && other == this
+}
+
+sealed class MultihopChange {
+ abstract val item: RelayItem
+
+ data class Entry(override val item: RelayItem) : MultihopChange()
+
+ data class Exit(override val item: RelayItem) : MultihopChange()
+}
+
+sealed interface ModifyMultihopError {
+ data class RelayItemInactive(val relayItem: RelayItem) : ModifyMultihopError
+
+ data class EntrySameAsExit(val relayItem: RelayItem) : ModifyMultihopError
+
+ data object GenericError : ModifyMultihopError
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt
index e4cf5c06bc..7357068a56 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCase.kt
@@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.usecase
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
@@ -24,10 +25,10 @@ class RecentsUseCase(
operator fun invoke(): Flow<List<Hop>?> =
combine(
recents(),
- filteredRelayListUseCase(RelayListType.ENTRY),
- customListsRelayItemUseCase(RelayListType.ENTRY),
- filteredRelayListUseCase(RelayListType.EXIT),
- customListsRelayItemUseCase(RelayListType.EXIT),
+ filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY)),
+ customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY)),
+ filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT)),
+ customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT)),
) { recents, entryRelayList, entryCustomLists, exitRelayList, exitCustomLists ->
recents?.mapNotNull { recent ->
when (recent) {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectHopUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectHopUseCase.kt
new file mode 100644
index 0000000000..487183a3c8
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/SelectHopUseCase.kt
@@ -0,0 +1,35 @@
+package net.mullvad.mullvadvpn.usecase
+
+import arrow.core.Either
+import arrow.core.raise.either
+import arrow.core.raise.ensure
+import net.mullvad.mullvadvpn.lib.model.Hop
+import net.mullvad.mullvadvpn.relaylist.isTheSameAs
+import net.mullvad.mullvadvpn.repository.RelayListRepository
+
+class SelectHopUseCase(private val relayListRepository: RelayListRepository) {
+ suspend operator fun invoke(hop: Hop): Either<SelectHopError, Unit> = either {
+ ensure(hop.isActive) { SelectHopError.HopInactive(hop = hop) }
+ when (hop) {
+ is Hop.Multi -> {
+ ensure(!hop.entry.isTheSameAs(hop.exit)) { SelectHopError.EntryAndExitSame }
+ relayListRepository
+ .updateSelectedRelayLocationMultihop(entry = hop.entry.id, exit = hop.exit.id)
+ .mapLeft { SelectHopError.GenericError }
+ }
+ is Hop.Single<*> -> {
+ relayListRepository.updateSelectedRelayLocation(hop.relay.id).mapLeft {
+ SelectHopError.GenericError
+ }
+ }
+ }
+ }
+}
+
+sealed interface SelectHopError {
+ data class HopInactive(val hop: Hop) : SelectHopError
+
+ data object EntryAndExitSame : SelectHopError
+
+ data object GenericError : SelectHopError
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt
index 42250ec1dc..243c0c643a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt
@@ -35,7 +35,6 @@ class FilterCustomListsRelayItemUseCase(
daita =
shouldFilterByDaita(
daitaDirectOnly = settings?.daitaAndDirectOnly() == true,
- isMultihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
relayListType = relayListType,
),
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt
index 8049bc2d28..e4a0a9957a 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/Daita.kt
@@ -1,12 +1,11 @@
package net.mullvad.mullvadvpn.util
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
-fun shouldFilterByDaita(
- daitaDirectOnly: Boolean,
- isMultihopEnabled: Boolean,
- relayListType: RelayListType,
-) =
- daitaDirectOnly &&
- (relayListType == RelayListType.ENTRY ||
- !isMultihopEnabled && relayListType == RelayListType.EXIT)
+fun shouldFilterByDaita(daitaDirectOnly: Boolean, relayListType: RelayListType) =
+ when (relayListType) {
+ RelayListType.Single -> daitaDirectOnly
+ is RelayListType.Multihop ->
+ daitaDirectOnly && relayListType.multihopRelayListType == MultihopRelayListType.ENTRY
+ }
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 64ccd99452..1d6691b755 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
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.viewmodel.location
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
@@ -486,10 +487,11 @@ internal fun RelayItemId.expandKey(parent: CustomListId? = null) =
internal fun RelayItemSelection.selectedByThisEntryExitList(relayListType: RelayListType) =
when (this) {
is RelayItemSelection.Multiple ->
- when (relayListType) {
- RelayListType.ENTRY -> entryLocation
- RelayListType.EXIT -> exitLocation
- }.getOrNull()
+ when ((relayListType as? RelayListType.Multihop)?.multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> entryLocation.getOrNull()
+ MultihopRelayListType.EXIT -> exitLocation.getOrNull()
+ else -> null
+ }
is RelayItemSelection.Single -> exitLocation.getOrNull()
}
@@ -500,10 +502,11 @@ internal fun RelayItemSelection.selectedByOtherEntryExitList(
when (this) {
is RelayItemSelection.Multiple -> {
val location =
- when (relayListType) {
- RelayListType.ENTRY -> exitLocation
- RelayListType.EXIT -> entryLocation
- }.getOrNull()
+ when ((relayListType as? RelayListType.Multihop)?.multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> exitLocation
+ MultihopRelayListType.EXIT -> entryLocation
+ else -> null
+ }?.getOrNull()
location.singleRelayId(customLists)
}
is RelayItemSelection.Single -> null
@@ -543,9 +546,10 @@ private fun RelayItem.createState(
is RelayItem.Location.Relay -> selectedByOtherId == id
}
return if (isSelectedByOther) {
- when (relayListType) {
- RelayListType.ENTRY -> RelayListItemState.USED_AS_EXIT
- RelayListType.EXIT -> RelayListItemState.USED_AS_ENTRY
+ when ((relayListType as? RelayListType.Multihop)?.multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> RelayListItemState.USED_AS_EXIT
+ MultihopRelayListType.EXIT -> RelayListItemState.USED_AS_ENTRY
+ else -> null
}
} else {
null
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 02cd8a765b..ce67ddba99 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
@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.model.Constraint
@@ -24,11 +25,15 @@ import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
-import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopError
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopUseCase
+import net.mullvad.mullvadvpn.usecase.MultihopChange
+import net.mullvad.mullvadvpn.usecase.SelectHopError
+import net.mullvad.mullvadvpn.usecase.SelectHopUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
@@ -39,11 +44,12 @@ import net.mullvad.mullvadvpn.util.combine
@Suppress("LongParameterList")
class SearchLocationViewModel(
private val wireguardConstraintsRepository: WireguardConstraintsRepository,
- private val relayListRepository: RelayListRepository,
private val customListActionUseCase: CustomListActionUseCase,
private val customListsRepository: CustomListsRepository,
private val relayListFilterRepository: RelayListFilterRepository,
private val filterChipUseCase: FilterChipUseCase,
+ private val selectHopUseCase: SelectHopUseCase,
+ private val modifyMultihopUseCase: ModifyMultihopUseCase,
filteredRelayListUseCase: FilteredRelayListUseCase,
filteredCustomListRelayItemsUseCase: FilterCustomListsRelayItemUseCase,
selectedLocationUseCase: SelectedLocationUseCase,
@@ -75,7 +81,7 @@ class SearchLocationViewModel(
filterChips,
expandOverrides ->
if (relayCountries.isEmpty()) {
- return@combine Lce.Error<Unit>(Unit)
+ return@combine Lce.Error(Unit)
}
val (expandSet, relayListLocations) =
searchRelayListLocations(
@@ -86,6 +92,7 @@ class SearchLocationViewModel(
Lce.Content(
SearchLocationUiState(
searchTerm = searchTerm,
+ relayListType = relayListType,
relayListItems =
relayListItemsSearching(
searchTerm = searchTerm,
@@ -118,31 +125,65 @@ class SearchLocationViewModel(
}
}
- fun selectHop(hop: Hop) {
+ fun selectRelayItem(relayItem: RelayItem, relayListType: RelayListType) {
viewModelScope.launch {
- if (hop.isActive) {
- selectRelayHop(
- hop = hop,
- relayListType = relayListType,
- selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
- selectExitLocation = relayListRepository::updateSelectedRelayLocation,
- selectMultihopLocation =
- relayListRepository::updateSelectedRelayLocationMultihop,
- )
- .fold(
- { _uiSideEffect.send(SearchLocationSideEffect.GenericError) },
- {
- _uiSideEffect.send(
- SearchLocationSideEffect.LocationSelected(relayListType)
- )
- },
+ when (relayListType) {
+ is RelayListType.Multihop ->
+ modifyMultihop(
+ when (relayListType.multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> MultihopChange.Entry(relayItem)
+ MultihopRelayListType.EXIT -> MultihopChange.Exit(relayItem)
+ }
)
- } else {
- _uiSideEffect.send(SearchLocationSideEffect.HopInactive(hop))
+ RelayListType.Single -> selectHop(hop = Hop.Single(relayItem))
}
}
}
+ private suspend fun selectHop(hop: Hop.Single<*>) =
+ selectHopUseCase(hop)
+ .fold(
+ {
+ _uiSideEffect.send(
+ when (it) {
+ SelectHopError.EntryAndExitSame ->
+ error("Entry and exit should not be the same when using Single hop")
+ SelectHopError.GenericError -> SearchLocationSideEffect.GenericError
+ is SelectHopError.HopInactive ->
+ SearchLocationSideEffect.RelayItemInactive(hop.relay)
+ }
+ )
+ },
+ { _uiSideEffect.send(SearchLocationSideEffect.LocationSelected(relayListType)) },
+ )
+
+ private suspend fun modifyMultihop(change: MultihopChange) =
+ modifyMultihopUseCase(change = change)
+ .fold(
+ {
+ _uiSideEffect.send(
+ when (it) {
+ is ModifyMultihopError.EntrySameAsExit ->
+ when (change) {
+ is MultihopChange.Entry ->
+ SearchLocationSideEffect.ExitAlreadySelected(
+ relayItem = change.item
+ )
+ is MultihopChange.Exit ->
+ SearchLocationSideEffect.EntryAlreadySelected(
+ relayItem = change.item
+ )
+ }
+ ModifyMultihopError.GenericError ->
+ SearchLocationSideEffect.GenericError
+ is ModifyMultihopError.RelayItemInactive ->
+ SearchLocationSideEffect.RelayItemInactive(relayItem = it.relayItem)
+ }
+ )
+ },
+ { _uiSideEffect.send(SearchLocationSideEffect.LocationSelected(relayListType)) },
+ )
+
private fun searchRelayListLocations(
searchTerm: String,
relayCountries: List<RelayItem.Location.Country>,
@@ -160,14 +201,12 @@ class SearchLocationViewModel(
wireguardConstraintsRepository.wireguardConstraints,
) { filterChips, constraints ->
filterChips.toMutableList().apply {
- // Do not show entry and exit filter chips if multihop is disabled
- if (constraints?.isMultihopEnabled == true) {
- add(
- when (relayListType) {
- RelayListType.ENTRY -> FilterChip.Entry
- RelayListType.EXIT -> FilterChip.Exit
- }
- )
+ // Only show entry and exit filter chips if relayListType is Multihop
+ if (relayListType is RelayListType.Multihop) {
+ when (relayListType.multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> add(FilterChip.Entry)
+ MultihopRelayListType.EXIT -> add(FilterChip.Exit)
+ }
}
}
}
@@ -228,7 +267,11 @@ sealed interface SearchLocationSideEffect {
data class CustomListActionToast(val resultData: CustomListActionResultData) :
SearchLocationSideEffect
- data class HopInactive(val hop: Hop) : SearchLocationSideEffect
+ data class RelayItemInactive(val relayItem: RelayItem) : SearchLocationSideEffect
+
+ data class EntryAlreadySelected(val relayItem: RelayItem) : SearchLocationSideEffect
+
+ data class ExitAlreadySelected(val relayItem: RelayItem) : SearchLocationSideEffect
data object GenericError : SearchLocationSideEffect
}
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 fc9cb77143..30e45d05cf 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
@@ -7,11 +7,13 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
@@ -42,11 +44,12 @@ class SelectLocationListViewModel(
customListsRelayItemUseCase(),
settingsRepository.settingsUpdates,
) { relayListItems, customLists, settings ->
- if (relayListType == RelayListType.ENTRY && settings?.entryBlocked() == true) {
+ if (settings.isBlocked()) {
Lce.Error(Unit)
} else {
Lce.Content(
SelectLocationListUiState(
+ relayListType = relayListType,
relayListItems = relayListItems,
customLists = customLists,
)
@@ -59,6 +62,14 @@ class SelectLocationListViewModel(
_expandedItems.onToggleExpandSet(item, parent, expand)
}
+ private fun Settings?.isBlocked(): Boolean =
+ when (relayListType) {
+ RelayListType.Single -> false
+ is RelayListType.Multihop ->
+ relayListType.multihopRelayListType == MultihopRelayListType.ENTRY &&
+ this?.entryBlocked() == true
+ }
+
private fun relayListItems() =
combine(
filteredRelayListUseCase(relayListType = relayListType),
@@ -116,8 +127,12 @@ class SelectLocationListViewModel(
private fun initialSelection() =
when (relayListType) {
- RelayListType.ENTRY ->
- wireguardConstraintsRepository.wireguardConstraints.value?.entryLocation
- RelayListType.EXIT -> relayListRepository.selectedLocation.value
+ RelayListType.Single -> relayListRepository.selectedLocation.value
+ is RelayListType.Multihop ->
+ when (relayListType.multihopRelayListType) {
+ MultihopRelayListType.ENTRY ->
+ wireguardConstraintsRepository.wireguardConstraints.value?.entryLocation
+ MultihopRelayListType.EXIT -> relayListRepository.selectedLocation.value
+ }
}?.getOrNull()
}
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 26f5dd42fa..768cf794eb 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
@@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.lib.model.Constraint
@@ -26,6 +27,11 @@ import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopError
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopUseCase
+import net.mullvad.mullvadvpn.usecase.MultihopChange
+import net.mullvad.mullvadvpn.usecase.SelectHopError
+import net.mullvad.mullvadvpn.usecase.SelectHopUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.util.Lc
@@ -35,13 +41,23 @@ class SelectLocationViewModel(
private val relayListFilterRepository: RelayListFilterRepository,
private val customListsRepository: CustomListsRepository,
private val customListActionUseCase: CustomListActionUseCase,
- private val relayListRepository: RelayListRepository,
- private val wireguardConstraintsRepository: WireguardConstraintsRepository,
+ relayListRepository: RelayListRepository,
+ wireguardConstraintsRepository: WireguardConstraintsRepository,
private val filterChipUseCase: FilterChipUseCase,
private val settingsRepository: SettingsRepository,
+ private val selectHopUseCase: SelectHopUseCase,
+ private val modifyMultihopUseCase: ModifyMultihopUseCase,
) : ViewModel() {
private val _relayListType: MutableStateFlow<RelayListType> =
- MutableStateFlow(RelayListType.EXIT)
+ MutableStateFlow(
+ if (
+ wireguardConstraintsRepository.wireguardConstraints.value?.isMultihopEnabled == true
+ ) {
+ RelayListType.Multihop(MultihopRelayListType.EXIT)
+ } else {
+ RelayListType.Single
+ }
+ )
val uiState =
combine(
@@ -57,9 +73,10 @@ class SelectLocationViewModel(
multihopEnabled = wireguardConstraints?.isMultihopEnabled == true,
relayListType = relayListSelection,
isSearchButtonEnabled =
- relayList.isNotEmpty() &&
- (relayListSelection == RelayListType.EXIT ||
- settings?.entryBlocked() != true),
+ searchButtonEnabled(
+ relayList = relayList,
+ relayListSelection = relayListSelection,
+ ),
isFilterButtonEnabled = relayList.isNotEmpty(),
isRecentsEnabled = settings?.recents is Recents.Enabled,
)
@@ -72,37 +89,58 @@ class SelectLocationViewModel(
private fun filterChips() = _relayListType.flatMapLatest { filterChipUseCase(it) }
- fun selectRelayList(relayListType: RelayListType) {
- viewModelScope.launch { _relayListType.emit(relayListType) }
+ private fun searchButtonEnabled(
+ relayList: List<RelayItem.Location.Country>,
+ relayListSelection: RelayListType,
+ ): Boolean {
+ val hasRelayListItems = relayList.isNotEmpty()
+ val isMultihopEntry =
+ relayListSelection is RelayListType.Multihop &&
+ relayListSelection.multihopRelayListType == MultihopRelayListType.ENTRY
+ val isEntryBlocked = settingsRepository.settingsUpdates.value?.entryBlocked() == true
+ return hasRelayListItems && !(isMultihopEntry && isEntryBlocked)
+ }
+
+ fun selectRelayList(multihopRelayListType: MultihopRelayListType) {
+ viewModelScope.launch { _relayListType.emit(RelayListType.Multihop(multihopRelayListType)) }
}
- fun selectHop(hop: Hop, relayListType: RelayListType) {
+ fun selectHop(hop: Hop) {
viewModelScope.launch {
- if (hop.isActive) {
- selectRelayHop(
- hop = hop,
- relayListType = relayListType,
- selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
- selectExitLocation = relayListRepository::updateSelectedRelayLocation,
- selectMultihopLocation =
- relayListRepository::updateSelectedRelayLocationMultihop,
- )
- .fold(
- { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
- {
- when (relayListType) {
- RelayListType.ENTRY ->
- if (hop is Hop.Multi)
- _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
- else _relayListType.emit(RelayListType.EXIT)
- RelayListType.EXIT ->
- _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
- }
- },
- )
- } else {
- _uiSideEffect.send(SelectLocationSideEffect.RelayItemInactive(hop))
+ selectHopUseCase(hop)
+ .fold(
+ { _uiSideEffect.send(it.toSideEffect()) },
+ { _uiSideEffect.send(SelectLocationSideEffect.CloseScreen) },
+ )
+ }
+ }
+
+ fun modifyMultihop(relayItem: RelayItem, multihopRelayListType: MultihopRelayListType) {
+ val change =
+ when (multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> MultihopChange.Entry(relayItem)
+ MultihopRelayListType.EXIT -> MultihopChange.Exit(relayItem)
}
+
+ viewModelScope.launch {
+ modifyMultihopUseCase(change)
+ .fold(
+ { _uiSideEffect.send(it.toSideEffect(multihopRelayListType)) },
+ {
+ when (multihopRelayListType) {
+ MultihopRelayListType.ENTRY -> {
+ _relayListType.emit(
+ RelayListType.Multihop(MultihopRelayListType.EXIT)
+ )
+ _uiSideEffect.send(
+ SelectLocationSideEffect.FocusExitList(relayItem)
+ )
+ }
+ MultihopRelayListType.EXIT ->
+ _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
+ }
+ },
+ )
}
}
@@ -149,6 +187,30 @@ class SelectLocationViewModel(
settingsRepository.setRecentsEnabled(!enabled)
}
}
+
+ private fun ModifyMultihopError.toSideEffect(
+ multihopRelayListType: MultihopRelayListType
+ ): SelectLocationSideEffect =
+ when (this) {
+ is ModifyMultihopError.EntrySameAsExit ->
+ when (multihopRelayListType) {
+ MultihopRelayListType.ENTRY ->
+ SelectLocationSideEffect.ExitAlreadySelected(relayItem = relayItem)
+ MultihopRelayListType.EXIT ->
+ SelectLocationSideEffect.EntryAlreadySelected(relayItem = relayItem)
+ }
+ ModifyMultihopError.GenericError -> SelectLocationSideEffect.GenericError
+ is ModifyMultihopError.RelayItemInactive ->
+ SelectLocationSideEffect.RelayItemInactive(hop = Hop.Single(this.relayItem))
+ }
+
+ private fun SelectHopError.toSideEffect(): SelectLocationSideEffect =
+ when (this) {
+ SelectHopError.GenericError -> SelectLocationSideEffect.GenericError
+ is SelectHopError.HopInactive ->
+ SelectLocationSideEffect.RelayItemInactive(hop = this.hop)
+ SelectHopError.EntryAndExitSame -> SelectLocationSideEffect.EntryAndExitAreSame
+ }
}
sealed interface SelectLocationSideEffect {
@@ -160,4 +222,12 @@ sealed interface SelectLocationSideEffect {
data object GenericError : SelectLocationSideEffect
data class RelayItemInactive(val hop: Hop) : SelectLocationSideEffect
+
+ data class EntryAlreadySelected(val relayItem: RelayItem) : SelectLocationSideEffect
+
+ data class ExitAlreadySelected(val relayItem: RelayItem) : SelectLocationSideEffect
+
+ data object EntryAndExitAreSame : SelectLocationSideEffect
+
+ data class FocusExitList(val relayItem: RelayItem) : SelectLocationSideEffect
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectRelay.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectRelay.kt
deleted file mode 100644
index f797501914..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectRelay.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.mullvad.mullvadvpn.viewmodel.location
-
-import arrow.core.Either
-import arrow.core.raise.either
-import net.mullvad.mullvadvpn.compose.state.RelayListType
-import net.mullvad.mullvadvpn.lib.model.Hop
-import net.mullvad.mullvadvpn.lib.model.RelayItemId
-
-internal suspend fun selectRelayHop(
- hop: Hop,
- relayListType: RelayListType,
- selectEntryLocation: suspend (RelayItemId) -> Either<Any, Unit>,
- selectExitLocation: suspend (RelayItemId) -> Either<Any, Unit>,
- selectMultihopLocation: suspend (RelayItemId, RelayItemId) -> Either<Any, Unit>,
-) =
- either<Any, Unit> {
- when (hop) {
- is Hop.Multi -> {
- val entryConstraint = hop.entry.id
- val exitConstraint = hop.exit.id
- selectMultihopLocation(entryConstraint, exitConstraint)
- }
-
- is Hop.Single<*> -> {
- val locationConstraint = hop.relay.id
- when (relayListType) {
- RelayListType.ENTRY -> selectEntryLocation(locationConstraint)
- RelayListType.EXIT -> selectExitLocation(locationConstraint)
- }
- }
- }
- }
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt
index ad1bc41a1b..69d40103a7 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/FilterChipUseCaseTest.kt
@@ -5,6 +5,7 @@ import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.common.test.assertLists
import net.mullvad.mullvadvpn.lib.model.Constraint
@@ -12,10 +13,8 @@ import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.ProviderId
import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.Settings
-import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
-import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
@@ -24,13 +23,11 @@ class FilterChipUseCaseTest {
private val mockRelayListFilterRepository: RelayListFilterRepository = mockk()
private val mockProviderToOwnershipsUseCase: ProviderToOwnershipsUseCase = mockk()
private val mockSettingRepository: SettingsRepository = mockk()
- private val mockWireguardConstraintsRepository: WireguardConstraintsRepository = mockk()
private val selectedOwnership = MutableStateFlow<Constraint<Ownership>>(Constraint.Any)
private val selectedProviders = MutableStateFlow<Constraint<Providers>>(Constraint.Any)
private val providerToOwnerships = MutableStateFlow<Map<ProviderId, Set<Ownership>>>(emptyMap())
private val settings = MutableStateFlow<Settings>(mockk(relaxed = true))
- private val wireguardConstraints = MutableStateFlow<WireguardConstraints>(mockk(relaxed = true))
private lateinit var filterChipUseCase: FilterChipUseCase
@@ -40,21 +37,18 @@ class FilterChipUseCaseTest {
every { mockRelayListFilterRepository.selectedProviders } returns selectedProviders
every { mockProviderToOwnershipsUseCase() } returns providerToOwnerships
every { mockSettingRepository.settingsUpdates } returns settings
- every { mockWireguardConstraintsRepository.wireguardConstraints } returns
- wireguardConstraints
filterChipUseCase =
FilterChipUseCase(
relayListFilterRepository = mockRelayListFilterRepository,
providerToOwnershipsUseCase = mockProviderToOwnershipsUseCase,
settingsRepository = mockSettingRepository,
- wireguardConstraintsRepository = mockWireguardConstraintsRepository,
)
}
@Test
fun `when no filters are applied should return empty list`() = runTest {
- filterChipUseCase(RelayListType.EXIT).test { assertLists(emptyList(), awaitItem()) }
+ filterChipUseCase(RelayListType.Single).test { assertLists(emptyList(), awaitItem()) }
}
@Test
@@ -63,7 +57,7 @@ class FilterChipUseCaseTest {
val expectedOwnership = Ownership.MullvadOwned
selectedOwnership.value = Constraint.Only(expectedOwnership)
- filterChipUseCase(RelayListType.EXIT).test {
+ filterChipUseCase(RelayListType.Single).test {
assertLists(listOf(FilterChip.Ownership(expectedOwnership)), awaitItem())
}
}
@@ -79,7 +73,7 @@ class FilterChipUseCaseTest {
ProviderId("2") to setOf(Ownership.Rented),
)
- filterChipUseCase(RelayListType.EXIT).test {
+ filterChipUseCase(RelayListType.Single).test {
assertLists(listOf(FilterChip.Provider(2)), awaitItem())
}
}
@@ -98,7 +92,7 @@ class FilterChipUseCaseTest {
ProviderId("2") to setOf(Ownership.Rented),
)
- filterChipUseCase(RelayListType.EXIT).test {
+ filterChipUseCase(RelayListType.Single).test {
assertLists(
listOf(FilterChip.Ownership(expectedOwnership), FilterChip.Provider(1)),
awaitItem(),
@@ -115,10 +109,8 @@ class FilterChipUseCaseTest {
every { this@mockk.tunnelOptions.wireguard.daitaSettings.enabled } returns true
every { tunnelOptions.wireguard.daitaSettings.directOnly } returns true
}
- wireguardConstraints.value =
- mockk(relaxed = true) { every { isMultihopEnabled } returns false }
- filterChipUseCase(RelayListType.EXIT).test {
+ filterChipUseCase(RelayListType.Single).test {
assertLists(listOf(FilterChip.Daita), awaitItem())
}
}
@@ -132,14 +124,12 @@ class FilterChipUseCaseTest {
every { tunnelOptions.wireguard.daitaSettings.enabled } returns true
every { tunnelOptions.wireguard.daitaSettings.directOnly } returns false
}
- wireguardConstraints.value =
- mockk(relaxed = true) { every { isMultihopEnabled } returns false }
- filterChipUseCase(RelayListType.EXIT).test { assertLists(emptyList(), awaitItem()) }
+ filterChipUseCase(RelayListType.Single).test { assertLists(emptyList(), awaitItem()) }
}
@Test
- fun `when Daita with direct only is enabled and multihop is enabled and relay list type is entry should return Daita filter chip`() =
+ fun `when Daita with direct only is enabled and relay list type is entry should return Daita filter chip`() =
runTest {
// Arrange
settings.value =
@@ -147,16 +137,14 @@ class FilterChipUseCaseTest {
every { tunnelOptions.wireguard.daitaSettings.enabled } returns true
every { tunnelOptions.wireguard.daitaSettings.directOnly } returns true
}
- wireguardConstraints.value =
- mockk(relaxed = true) { every { isMultihopEnabled } returns true }
- filterChipUseCase(RelayListType.ENTRY).test {
+ filterChipUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY)).test {
assertLists(listOf(FilterChip.Daita), awaitItem())
}
}
@Test
- fun `when Daita with direct only is enabled and multihop is enabled and relay list type is exit should return no filter`() =
+ fun `when Daita with direct only is enabled and relay list type is exit should return no filter`() =
runTest {
// Arrange
settings.value =
@@ -164,14 +152,14 @@ class FilterChipUseCaseTest {
every { tunnelOptions.wireguard.daitaSettings.enabled } returns true
every { tunnelOptions.wireguard.daitaSettings.directOnly } returns true
}
- wireguardConstraints.value =
- mockk(relaxed = true) { every { isMultihopEnabled } returns true }
- filterChipUseCase(RelayListType.EXIT).test { assertLists(emptyList(), awaitItem()) }
+ filterChipUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT)).test {
+ assertLists(emptyList(), awaitItem())
+ }
}
@Test
- fun `when Daita without direct only is enabled and multihop is enabled and relay list type is exit should return no filter`() =
+ fun `when Daita without direct only is enabled and relay list type is exit should return no filter`() =
runTest {
// Arrange
settings.value =
@@ -179,10 +167,10 @@ class FilterChipUseCaseTest {
every { tunnelOptions.wireguard.daitaSettings.enabled } returns true
every { tunnelOptions.wireguard.daitaSettings.directOnly } returns false
}
- wireguardConstraints.value =
- mockk(relaxed = true) { every { isMultihopEnabled } returns true }
- filterChipUseCase(RelayListType.EXIT).test { assertLists(emptyList(), awaitItem()) }
+ filterChipUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT)).test {
+ assertLists(emptyList(), awaitItem())
+ }
}
@Test
@@ -200,7 +188,7 @@ class FilterChipUseCaseTest {
)
// Act, Assert
- filterChipUseCase(RelayListType.EXIT).test {
+ filterChipUseCase(RelayListType.Single).test {
assertLists(
listOf(FilterChip.Ownership(expectedOwnership), FilterChip.Provider(1)),
awaitItem(),
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt
index f7699101d5..9623fffc7c 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/RecentsUseCaseTest.kt
@@ -8,6 +8,7 @@ import kotlin.test.assertNull
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.compose.state.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.lib.model.CustomList
import net.mullvad.mullvadvpn.lib.model.CustomListId
@@ -118,13 +119,18 @@ class RecentsUseCaseTest {
Recents.Enabled(listOf(singleHopRecent, multiHopRecent, filteredOutRecent))
}
- every { customListsRelayItemUseCase(RelayListType.ENTRY) } returns
- flowOf(listOf(entryCustomList))
- every { customListsRelayItemUseCase(RelayListType.EXIT) } returns flowOf(emptyList())
- every { filteredRelayListUseCase(RelayListType.ENTRY) } returns
- flowOf(listOf(sweden, norway))
- every { filteredRelayListUseCase(RelayListType.EXIT) } returns
- flowOf(listOf(sweden, norway))
+ every {
+ customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY))
+ } returns flowOf(listOf(entryCustomList))
+ every {
+ customListsRelayItemUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT))
+ } returns flowOf(emptyList())
+ every {
+ filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.ENTRY))
+ } returns flowOf(listOf(sweden, norway))
+ every {
+ filteredRelayListUseCase(RelayListType.Multihop(MultihopRelayListType.EXIT))
+ } returns flowOf(listOf(sweden, norway))
useCase().test {
val hops = 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 ad0f87638f..8aa1e9b038 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
@@ -20,11 +20,12 @@ import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.RelayListFilterRepository
-import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
import net.mullvad.mullvadvpn.usecase.FilteredRelayListUseCase
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopUseCase
+import net.mullvad.mullvadvpn.usecase.SelectHopUseCase
import net.mullvad.mullvadvpn.usecase.SelectedLocationUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListsRelayItemUseCase
@@ -39,7 +40,6 @@ import org.junit.jupiter.api.extension.ExtendWith
class SearchLocationViewModelTest {
private val mockWireguardConstraintsRepository: WireguardConstraintsRepository = mockk()
- private val mockRelayListRepository: RelayListRepository = mockk()
private val mockFilteredRelayListUseCase: FilteredRelayListUseCase = mockk()
private val mockCustomListActionUseCase: CustomListActionUseCase = mockk()
private val mockCustomListsRepository: CustomListsRepository = mockk()
@@ -48,6 +48,8 @@ class SearchLocationViewModelTest {
private val mockFilteredCustomListRelayItemsUseCase: FilterCustomListsRelayItemUseCase = mockk()
private val mockSelectedLocationUseCase: SelectedLocationUseCase = mockk()
private val mockCustomListsRelayItemUseCase: CustomListsRelayItemUseCase = mockk()
+ private val mockSelectHopUseCase: SelectHopUseCase = mockk()
+ private val mockModifyMultihopUseCase: ModifyMultihopUseCase = mockk()
private val filteredRelayList = MutableStateFlow<List<RelayItem.Location.Country>>(emptyList())
private val selectedLocation =
@@ -74,7 +76,6 @@ class SearchLocationViewModelTest {
viewModel =
SearchLocationViewModel(
wireguardConstraintsRepository = mockWireguardConstraintsRepository,
- relayListRepository = mockRelayListRepository,
filteredRelayListUseCase = mockFilteredRelayListUseCase,
customListActionUseCase = mockCustomListActionUseCase,
customListsRepository = mockCustomListsRepository,
@@ -83,8 +84,10 @@ class SearchLocationViewModelTest {
filteredCustomListRelayItemsUseCase = mockFilteredCustomListRelayItemsUseCase,
selectedLocationUseCase = mockSelectedLocationUseCase,
customListsRelayItemUseCase = mockCustomListsRelayItemUseCase,
+ selectHopUseCase = mockSelectHopUseCase,
+ modifyMultihopUseCase = mockModifyMultihopUseCase,
savedStateHandle =
- SearchLocationNavArgs(relayListType = RelayListType.ENTRY).toSavedStateHandle(),
+ SearchLocationNavArgs(relayListType = RelayListType.Single).toSavedStateHandle(),
)
}
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 1a54d15f95..3f9bfe751a 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
@@ -73,7 +73,7 @@ class SelectLocationListViewModelTest {
@Test
fun `initial state should be loading`() = runTest {
// Arrange
- viewModel = createSelectLocationListViewModel(relayListType = RelayListType.ENTRY)
+ viewModel = createSelectLocationListViewModel(relayListType = RelayListType.Single)
// Assert
assertEquals(Lce.Loading(Unit), viewModel.uiState.value)
@@ -82,7 +82,7 @@ class SelectLocationListViewModelTest {
@Test
fun `given filteredRelayList emits update uiState should contain new update`() = runTest {
// Arrange
- viewModel = createSelectLocationListViewModel(RelayListType.EXIT)
+ viewModel = createSelectLocationListViewModel(RelayListType.Single)
filteredRelayList.value = testCountries
val selectedId = testCountries.first().id
selectedLocationFlow.value = RelayItemSelection.Single(Constraint.Only(selectedId))
@@ -107,7 +107,7 @@ class SelectLocationListViewModelTest {
@Test
fun `given relay is not selected all relay items should not be selected`() = runTest {
// Arrange
- viewModel = createSelectLocationListViewModel(RelayListType.EXIT)
+ viewModel = createSelectLocationListViewModel(RelayListType.Single)
filteredRelayList.value = testCountries
selectedLocationFlow.value = RelayItemSelection.Single(Constraint.Any)
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 e5804ffe61..e50cfb48a2 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
@@ -17,6 +17,7 @@ import kotlinx.coroutines.test.runTest
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.MultihopRelayListType
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
@@ -25,7 +26,6 @@ import net.mullvad.mullvadvpn.lib.model.CustomList
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
-import net.mullvad.mullvadvpn.lib.model.Hop
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.Providers
import net.mullvad.mullvadvpn.lib.model.RelayItem
@@ -40,6 +40,9 @@ import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.usecase.FilterChipUseCase
+import net.mullvad.mullvadvpn.usecase.ModifyMultihopUseCase
+import net.mullvad.mullvadvpn.usecase.MultihopChange
+import net.mullvad.mullvadvpn.usecase.SelectHopUseCase
import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase
import net.mullvad.mullvadvpn.util.Lc
import org.junit.jupiter.api.AfterEach
@@ -57,6 +60,8 @@ class SelectLocationViewModelTest {
private val mockWireguardConstraintsRepository: WireguardConstraintsRepository = mockk()
private val mockFilterChipUseCase: FilterChipUseCase = mockk()
private val mockSettingsRepository: SettingsRepository = mockk()
+ private val mockSelectHopUseCase: SelectHopUseCase = mockk()
+ private val mockModifyMultihopUseCase: ModifyMultihopUseCase = mockk()
private lateinit var viewModel: SelectLocationViewModel
@@ -88,6 +93,8 @@ class SelectLocationViewModelTest {
filterChipUseCase = mockFilterChipUseCase,
wireguardConstraintsRepository = mockWireguardConstraintsRepository,
settingsRepository = mockSettingsRepository,
+ modifyMultihopUseCase = mockModifyMultihopUseCase,
+ selectHopUseCase = mockSelectHopUseCase,
)
}
@@ -103,22 +110,22 @@ class SelectLocationViewModelTest {
}
@Test
- fun `on selectRelay when relay list type is exit call uiSideEffect should emit CloseScreen and connect`() =
+ fun `on modifyMultihop when relay list type is exit call uiSideEffect should emit CloseScreen and connect`() =
runTest {
// Arrange
val mockRelayItem: RelayItem.Location.Country = mockk()
val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
+ val multihopChange: MultihopChange = MultihopChange.Exit(mockRelayItem)
every { mockRelayItem.id } returns relayItemId
every { mockRelayItem.active } returns true
- coEvery { mockRelayListRepository.updateSelectedRelayLocation(relayItemId) } returns
- Unit.right()
+ coEvery { mockModifyMultihopUseCase.invoke(multihopChange) } returns Unit.right()
// Act, Assert
viewModel.uiSideEffect.test {
- viewModel.selectHop(Hop.Single(mockRelayItem), RelayListType.EXIT)
+ viewModel.modifyMultihop(mockRelayItem, MultihopRelayListType.EXIT)
// Await an empty item
assertEquals(SelectLocationSideEffect.CloseScreen, awaitItem())
- coVerify { mockRelayListRepository.updateSelectedRelayLocation(relayItemId) }
+ coVerify { mockModifyMultihopUseCase.invoke(multihopChange) }
}
}
@@ -128,26 +135,32 @@ class SelectLocationViewModelTest {
// Arrange
val mockRelayItem: RelayItem.Location.Country = mockk()
val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
+ val multihopChange = MultihopChange.Entry(mockRelayItem)
every { mockRelayItem.active } returns true
every { mockRelayItem.id } returns relayItemId
- coEvery { mockWireguardConstraintsRepository.setEntryLocation(relayItemId) } returns
- Unit.right()
+ coEvery { mockModifyMultihopUseCase.invoke(multihopChange) } returns Unit.right()
// Act, Assert
viewModel.uiState.test {
awaitItem() // Default value
- viewModel.selectRelayList(RelayListType.ENTRY)
+ viewModel.selectRelayList(MultihopRelayListType.ENTRY)
// Assert relay list type is entry
val firstState = awaitItem()
assertIs<Lc.Content<SelectLocationUiState>>(firstState)
- assertEquals(RelayListType.ENTRY, firstState.value.relayListType)
+ assertEquals(
+ RelayListType.Multihop(MultihopRelayListType.ENTRY),
+ firstState.value.relayListType,
+ )
// Select entry
- viewModel.selectHop(Hop.Single(mockRelayItem), RelayListType.ENTRY)
+ viewModel.modifyMultihop(mockRelayItem, MultihopRelayListType.ENTRY)
// Assert relay list type is exit
val secondState = awaitItem()
assertIs<Lc.Content<SelectLocationUiState>>(secondState)
- assertEquals(RelayListType.EXIT, secondState.value.relayListType)
- coVerify { mockWireguardConstraintsRepository.setEntryLocation(relayItemId) }
+ assertEquals(
+ RelayListType.Multihop(MultihopRelayListType.EXIT),
+ secondState.value.relayListType,
+ )
+ coVerify { mockModifyMultihopUseCase.invoke(multihopChange) }
}
}
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index abff8ca10c..c58d62fc03 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -433,4 +433,7 @@
<string name="no_recent_selection">No recent selection history</string>
<string name="recents_disabled">Recents disabled and history cleared</string>
<string name="more_actions">More actions</string>
+ <string name="relay_item_already_selected_as_exit">%s is already selected as exit relay</string>
+ <string name="relay_item_already_selected_as_entry">%s is already selected as entry relay</string>
+ <string name="entry_and_exit_are_same">Entry relay is the same as exit relay</string>
</resources>
diff --git a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt
index 8c38610f9e..76fb8e2f33 100644
--- a/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt
+++ b/android/lib/ui/component/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/component/relaylist/SelectableRelayListItem.kt
@@ -38,7 +38,6 @@ import net.mullvad.mullvadvpn.lib.resource.R
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.lib.theme.color.selected
import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevron
import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListItem
import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListItemDefaults
@@ -118,10 +117,7 @@ fun SelectableRelayListItem(
)
}
},
- onClick =
- if (relayListItem.state == null) onClick
- /* Workaround for not allowing to select relay that is currently used as entry or exit */
- else ({}),
+ onClick = onClick,
onLongClick = onLongClick,
trailingContent =
if (relayListItem.canExpand) {