summaryrefslogtreecommitdiffhomepage
path: root/android/app
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-07-04 16:12:32 +0200
committerDavid Göransson <david.goransson@mullvad.net>2025-07-04 16:12:32 +0200
commit5300f1663559ebd7a87c699db8e858d13e6fa556 (patch)
tree0081e14129def76d6a57b32232e42411c2fbe10d /android/app
parent3e5795cbab6866fcd3eafee58d1790fc9e7f1829 (diff)
parent0d5660226494abaf04dc619997bf4d6a27c637d8 (diff)
downloadmullvadvpn-5300f1663559ebd7a87c699db8e858d13e6fa556.tar.xz
mullvadvpn-5300f1663559ebd7a87c699db8e858d13e6fa556.zip
Merge branch 'implement-new-select-location-design-droid-1954'
Diffstat (limited to 'android/app')
-rw-r--r--android/app/build.gradle.kts1
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt8
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreenTest.kt4
-rw-r--r--android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreenTest.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt273
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt69
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt43
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt28
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt65
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt34
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt120
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreen.kt44
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt75
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/RelayListContent.kt225
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SearchLocationScreen.kt18
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationList.kt15
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt11
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt95
-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/viewmodel/CustomListLocationsViewModel.kt84
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/RelayItemListCreator.kt66
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModel.kt30
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModel.kt39
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt50
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SearchLocationViewModelTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationListViewModelTest.kt2
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/location/SelectLocationViewModelTest.kt2
34 files changed, 387 insertions, 1082 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 32ed53060b..24172279fc 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -348,6 +348,7 @@ dependencies {
implementation(projects.lib.shared)
implementation(projects.lib.talpid)
implementation(projects.lib.tv)
+ implementation(projects.lib.ui.designsystem)
implementation(projects.lib.ui.component)
implementation(projects.lib.ui.tag)
implementation(projects.tile)
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
index d556fe5d10..a7bed534ba 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt
@@ -14,8 +14,8 @@ import net.mullvad.mullvadvpn.compose.data.DUMMY_RELAY_COUNTRIES
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
import net.mullvad.mullvadvpn.lib.ui.tag.CIRCULAR_PROGRESS_INDICATOR_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SAVE_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.util.Lce
@@ -104,11 +104,11 @@ class CustomListLocationsScreenTest {
CustomListLocationsData(
locations =
listOf(
- RelayLocationListItem(
+ CheckableRelayListItem(
DUMMY_RELAY_COUNTRIES[0],
checked = true,
),
- RelayLocationListItem(
+ CheckableRelayListItem(
DUMMY_RELAY_COUNTRIES[1],
checked = false,
),
@@ -141,7 +141,7 @@ class CustomListLocationsScreenTest {
CustomListLocationsData(
locations =
listOf(
- RelayLocationListItem(selectedCountry, checked = true)
+ CheckableRelayListItem(selectedCountry, checked = true)
),
searchTerm = "",
saveEnabled = false,
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 32a91fa61b..7e03afb18c 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,11 +12,11 @@ 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.RelayListItem
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.util.Lce
import org.junit.jupiter.api.AfterEach
@@ -44,7 +44,6 @@ class SearchLocationScreenTest {
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit = { _, _, _ -> },
onSearchInputChanged: (String) -> Unit = {},
onCreateCustomList: (location: RelayItem.Location?) -> Unit = {},
- onEditCustomLists: () -> Unit = {},
onAddLocationToList:
(location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit =
{ _, _ ->
@@ -67,7 +66,6 @@ class SearchLocationScreenTest {
onToggleExpand = onToggleExpand,
onSearchInputChanged = onSearchInputChanged,
onCreateCustomList = onCreateCustomList,
- onEditCustomLists = onEditCustomLists,
onAddLocationToList = onAddLocationToList,
onRemoveLocationFromList = onRemoveLocationFromList,
onEditCustomListName = onEditCustomListName,
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 e03786aef3..0767fc35ad 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,12 +15,13 @@ 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.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.performLongClick
@@ -110,7 +111,10 @@ class SelectLocationScreenTest {
SelectLocationListUiState(
relayListItems =
DUMMY_RELAY_COUNTRIES.map {
- RelayListItem.GeoLocationItem(item = it)
+ RelayListItem.GeoLocationItem(
+ item = it,
+ itemPosition = ItemPosition.Single,
+ )
},
customLists = emptyList(),
)
@@ -250,7 +254,13 @@ class SelectLocationScreenTest {
MutableStateFlow(
Lce.Content(
SelectLocationListUiState(
- relayListItems = listOf(RelayListItem.GeoLocationItem(relayItem)),
+ relayListItems =
+ listOf(
+ RelayListItem.GeoLocationItem(
+ relayItem,
+ itemPosition = ItemPosition.Single,
+ )
+ ),
customLists = emptyList(),
)
)
@@ -278,6 +288,6 @@ class SelectLocationScreenTest {
}
companion object {
- private const val CUSTOM_LISTS_EMPTY_TEXT = "To create a custom list press the \"︙\""
+ private const val CUSTOM_LISTS_EMPTY_TEXT = "To create a custom list press the \"+\""
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
index a3fbd4d94d..633b32e420 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/CheckboxCell.kt
@@ -14,9 +14,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
-import net.mullvad.mullvadvpn.compose.component.MullvadCheckbox
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.ui.designsystem.Checkbox
@Preview
@Composable
@@ -46,7 +46,7 @@ internal fun CheckboxCell(
.background(background)
.padding(start = startPadding, end = endPadding),
) {
- MullvadCheckbox(checked = checked, onCheckedChange = onCheckedChange)
+ Checkbox(checked = checked, onCheckedChange = onCheckedChange)
Text(
text = title,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt
index 536a7ef8b8..a811682767 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/ExpandableComposeCell.kt
@@ -19,11 +19,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.component.ExpandChevronIconButton
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
+import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevronIconButton
@Preview
@Composable
@@ -103,11 +103,7 @@ private fun ExpandableComposeCellBody(
}
}
- ExpandChevronIconButton(
- isExpanded = isExpanded,
- onExpand = onExpand,
- color = MaterialTheme.colorScheme.onPrimary,
- )
+ ExpandChevronIconButton(isExpanded = isExpanded, onExpand = onExpand)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
deleted file mode 100644
index 45fd0d3bd0..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/cell/RelayLocationCell.kt
+++ /dev/null
@@ -1,273 +0,0 @@
-package net.mullvad.mullvadvpn.compose.cell
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material3.Icon
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.VerticalDivider
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.tooling.preview.PreviewParameter
-import androidx.compose.ui.unit.dp
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.component.ExpandChevron
-import net.mullvad.mullvadvpn.compose.component.MullvadCheckbox
-import net.mullvad.mullvadvpn.compose.preview.RelayItemCheckableCellPreviewParameterProvider
-import net.mullvad.mullvadvpn.compose.state.RelayListItemState
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.Dimens
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.lib.theme.color.selected
-import net.mullvad.mullvadvpn.lib.ui.tag.EXPAND_BUTTON_TEST_TAG
-import net.mullvad.mullvadvpn.lib.ui.tag.LOCATION_CELL_TEST_TAG
-
-@Composable
-@Preview
-private fun PreviewCheckableRelayLocationCell(
- @PreviewParameter(RelayItemCheckableCellPreviewParameterProvider::class)
- relayItems: List<RelayItem.Location.Country>
-) {
- AppTheme {
- Column(Modifier.background(color = MaterialTheme.colorScheme.surface)) {
- relayItems.map {
- CheckableRelayLocationCell(
- item = it,
- checked = false,
- expanded = false,
- depth = 0,
- onExpand = {},
- modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
- )
- }
- }
- }
-}
-
-@Composable
-fun StatusRelayItemCell(
- item: RelayItem,
- isSelected: Boolean,
- state: RelayListItemState?,
- modifier: Modifier = Modifier,
- onClick: () -> Unit,
- onLongClick: (() -> Unit)? = null,
- onToggleExpand: ((Boolean) -> Unit),
- isExpanded: Boolean = false,
- depth: Int = 0,
- activeColor: Color = MaterialTheme.colorScheme.selected,
- inactiveColor: Color = MaterialTheme.colorScheme.error,
- disabledColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,
-) {
- RelayItemCell(
- modifier = modifier,
- item = item,
- isSelected = isSelected,
- state = state,
- onClick = onClick,
- onLongClick = onLongClick,
- onToggleExpand = onToggleExpand,
- isExpanded = isExpanded,
- depth = depth,
- content = {
- if (isSelected) {
- Icon(imageVector = Icons.Default.Check, contentDescription = null)
- } else {
- Box(
- modifier =
- Modifier.padding(4.dp)
- .size(Dimens.relayCircleSize)
- .background(
- color =
- when {
- item is RelayItem.CustomList && item.locations.isEmpty() ->
- disabledColor
- state != null -> disabledColor
- item.active -> activeColor
- else -> inactiveColor
- },
- shape = CircleShape,
- )
- )
- }
- },
- )
-}
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun RelayItemCell(
- modifier: Modifier = Modifier,
- item: RelayItem,
- isSelected: Boolean,
- state: RelayListItemState?,
- onClick: () -> Unit,
- onLongClick: (() -> Unit)? = null,
- onToggleExpand: (Boolean) -> Unit,
- isExpanded: Boolean,
- depth: Int,
- content: @Composable (RowScope.() -> Unit)? = null,
-) {
-
- val leadingContentStartPadding = Dimens.cellStartPadding
- val leadingContentStarPaddingModifier = Dimens.mediumPadding
- val startPadding = leadingContentStartPadding + leadingContentStarPaddingModifier * depth
- Row(
- modifier =
- modifier
- .fillMaxWidth()
- .height(IntrinsicSize.Min)
- .background(
- when {
- isSelected -> MaterialTheme.colorScheme.selected
- else -> depth.toBackgroundColor()
- }
- ),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- // Duplicate row is needed for selection of the item on TV.
- Row(
- modifier =
- Modifier.combinedClickable(
- enabled = state == null && item.active,
- onClick = onClick,
- onLongClick = onLongClick,
- )
- .padding(start = startPadding)
- .weight(1f),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- if (content != null) {
- content()
- }
- Name(name = item.name, state = state, active = item.active)
- }
-
- if (item.hasChildren) {
- ExpandButton(
- color = MaterialTheme.colorScheme.onSurface,
- isExpanded = isExpanded,
- onClick = { onToggleExpand(!isExpanded) },
- modifier = Modifier.testTag(EXPAND_BUTTON_TEST_TAG),
- )
- }
- }
-}
-
-@Composable
-fun CheckableRelayLocationCell(
- item: RelayItem,
- modifier: Modifier = Modifier,
- checked: Boolean,
- onRelayCheckedChange: (isChecked: Boolean) -> Unit = { _ -> },
- expanded: Boolean,
- onExpand: (Boolean) -> Unit,
- depth: Int,
-) {
- RelayItemCell(
- modifier = modifier,
- item = item,
- isSelected = false,
- state = null,
- onClick = { onRelayCheckedChange(!checked) },
- onToggleExpand = onExpand,
- isExpanded = expanded,
- depth = depth,
- content = {
- MullvadCheckbox(
- checked = checked,
- onCheckedChange = { isChecked -> onRelayCheckedChange(isChecked) },
- )
- },
- )
-}
-
-@Composable
-private fun Name(
- modifier: Modifier = Modifier,
- name: String,
- state: RelayListItemState?,
- active: Boolean,
-) {
- Text(
- text = state?.let { name.withSuffix(state) } ?: name,
- color = MaterialTheme.colorScheme.onSurface,
- modifier =
- modifier
- .alpha(
- if (state == null && active) {
- AlphaVisible
- } else {
- AlphaInactive
- }
- )
- .padding(horizontal = Dimens.smallPadding, vertical = Dimens.mediumPadding),
- maxLines = 1,
- style = MaterialTheme.typography.bodyLarge,
- overflow = TextOverflow.Ellipsis,
- )
-}
-
-@Composable
-private fun RowScope.ExpandButton(
- modifier: Modifier,
- color: Color,
- isExpanded: Boolean,
- onClick: (expand: Boolean) -> Unit,
-) {
- VerticalDivider(
- color = MaterialTheme.colorScheme.surface,
- modifier = Modifier.padding(vertical = Dimens.verticalDividerPadding),
- )
- ExpandChevron(
- color = color,
- isExpanded = isExpanded,
- modifier =
- modifier
- .fillMaxHeight()
- .clickable { onClick(!isExpanded) }
- .padding(horizontal = Dimens.largePadding)
- .align(Alignment.CenterVertically),
- )
-}
-
-@Suppress("MagicNumber")
-@Composable
-private fun Int.toBackgroundColor(): Color =
- when (this) {
- 0 -> MaterialTheme.colorScheme.surfaceContainerHighest
- 1 -> MaterialTheme.colorScheme.surfaceContainerHigh
- 2 -> MaterialTheme.colorScheme.surfaceContainerLow
- else -> MaterialTheme.colorScheme.surfaceContainerLowest
- }
-
-@Composable
-private fun String.withSuffix(state: RelayListItemState) =
- when (state) {
- RelayListItemState.USED_AS_EXIT -> stringResource(R.string.x_exit, this)
- RelayListItemState.USED_AS_ENTRY -> stringResource(R.string.x_entry, this)
- }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt
deleted file mode 100644
index d1ec86fde7..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/ExpandChevron.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package net.mullvad.mullvadvpn.compose.component
-
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.TweenSpec
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.KeyboardArrowDown
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import net.mullvad.mullvadvpn.R
-
-@Composable
-@Preview
-private fun PreviewChevron() {
- Column {
- ExpandChevron(color = MaterialTheme.colorScheme.onPrimary, isExpanded = false)
- ExpandChevron(color = MaterialTheme.colorScheme.onPrimary, isExpanded = true)
- }
-}
-
-@Composable
-fun ExpandChevron(modifier: Modifier = Modifier, color: Color, isExpanded: Boolean) {
-
- val degree = remember(isExpanded) { if (isExpanded) UP_ROTATION else DOWN_ROTATION }
- val stateLabel =
- if (isExpanded) {
- stringResource(id = R.string.collapse)
- } else {
- stringResource(id = R.string.expand)
- }
- val animatedRotation =
- animateFloatAsState(
- targetValue = degree,
- label = "",
- animationSpec = TweenSpec(ROTATION_ANIMATION_DURATION, easing = LinearEasing),
- )
-
- Icon(
- imageVector = Icons.Default.KeyboardArrowDown,
- contentDescription = stateLabel,
- tint = color,
- modifier = modifier.rotate(animatedRotation.value),
- )
-}
-
-@Composable
-fun ExpandChevronIconButton(
- modifier: Modifier = Modifier,
- color: Color,
- onExpand: (Boolean) -> Unit,
- isExpanded: Boolean,
-) {
- IconButton(modifier = modifier, onClick = { onExpand(!isExpanded) }) {
- ExpandChevron(isExpanded = isExpanded, color = color)
- }
-}
-
-private const val DOWN_ROTATION = 0f
-private const val UP_ROTATION = 180f
-private const val ROTATION_ANIMATION_DURATION = 100
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt
deleted file mode 100644
index 8ff5ae1df1..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadCheckbox.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package net.mullvad.mullvadvpn.compose.component
-
-import androidx.compose.foundation.background
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.CheckboxDefaults
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.tooling.preview.Preview
-import net.mullvad.mullvadvpn.lib.theme.AppTheme
-import net.mullvad.mullvadvpn.lib.theme.color.selected
-
-@Preview
-@Composable
-private fun PreviewMullvadCheckbox() {
- AppTheme {
- SpacedColumn(Modifier.background(color = MaterialTheme.colorScheme.primary)) {
- MullvadCheckbox(checked = false) {}
- MullvadCheckbox(checked = true) {}
- }
- }
-}
-
-@Composable
-fun MullvadCheckbox(
- checkedColor: Color = MaterialTheme.colorScheme.onPrimary,
- uncheckedColor: Color = MaterialTheme.colorScheme.onPrimary,
- checkmarkColor: Color = MaterialTheme.colorScheme.selected,
- checked: Boolean,
- onCheckedChange: (Boolean) -> Unit,
-) {
- Checkbox(
- checked = checked,
- onCheckedChange = onCheckedChange,
- colors =
- CheckboxDefaults.colors(
- checkedColor = checkedColor,
- uncheckedColor = uncheckedColor,
- checkmarkColor = checkmarkColor,
- ),
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
index 514005095b..d17a42d76f 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/CustomListLocationUiStatePreviewParameterProvider.kt
@@ -3,8 +3,9 @@ package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.generateRelayItemCountry
import net.mullvad.mullvadvpn.util.Lce
class CustomListLocationUiStatePreviewParameterProvider :
@@ -18,7 +19,7 @@ class CustomListLocationUiStatePreviewParameterProvider :
CustomListLocationsData(
locations =
listOf(
- RelayLocationListItem(
+ CheckableRelayListItem(
item =
generateRelayItemCountry(
name = "A relay",
@@ -27,7 +28,7 @@ class CustomListLocationUiStatePreviewParameterProvider :
active = true,
)
),
- RelayLocationListItem(
+ CheckableRelayListItem(
item =
generateRelayItemCountry(
name = "Another relay",
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt
deleted file mode 100644
index c0d4ddc063..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemCheckableCellPreviewParameterProvider.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package net.mullvad.mullvadvpn.compose.preview
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-
-class RelayItemCheckableCellPreviewParameterProvider :
- PreviewParameterProvider<List<RelayItem.Location.Country>> {
- override val values =
- sequenceOf(
- listOf(
- generateRelayItemCountry(
- name = "Relay country Active",
- cityNames = listOf("Relay city 1", "Relay city 2"),
- relaysPerCity = 2,
- ),
- generateRelayItemCountry(
- name = "Relay country Expanded",
- cityNames = listOf("Normal city"),
- relaysPerCity = 2,
- ),
- generateRelayItemCountry(
- name = "Country and city Expanded",
- cityNames = listOf("Expanded city A", "Expanded city B"),
- relaysPerCity = 2,
- ),
- )
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt
deleted file mode 100644
index 3d706443d2..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package net.mullvad.mullvadvpn.compose.preview
-
-import net.mullvad.mullvadvpn.lib.model.GeoLocationId
-import net.mullvad.mullvadvpn.lib.model.Ownership
-import net.mullvad.mullvadvpn.lib.model.ProviderId
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-
-fun generateRelayItemCountry(
- name: String,
- cityNames: List<String>,
- relaysPerCity: Int,
- active: Boolean = true,
-) =
- RelayItem.Location.Country(
- name = name,
- id = name.generateCountryCode(),
- cities =
- cityNames.map { cityName ->
- generateRelayItemCity(cityName, name.generateCountryCode(), relaysPerCity, active)
- },
- )
-
-private fun generateRelayItemCity(
- name: String,
- countryCode: GeoLocationId.Country,
- numberOfRelays: Int,
- active: Boolean = true,
-) =
- RelayItem.Location.City(
- name = name,
- id = name.generateCityCode(countryCode),
- relays =
- List(numberOfRelays) { index ->
- generateRelayItemRelay(
- name.generateCityCode(countryCode),
- generateHostname(name.generateCityCode(countryCode), index),
- active,
- )
- },
- )
-
-private fun generateRelayItemRelay(
- cityCode: GeoLocationId.City,
- hostName: String,
- active: Boolean = true,
- daita: Boolean = true,
-) =
- RelayItem.Location.Relay(
- id = GeoLocationId.Hostname(city = cityCode, code = hostName),
- active = active,
- provider = ProviderId("Provider"),
- ownership = Ownership.MullvadOwned,
- daita = daita,
- )
-
-private fun String.generateCountryCode() =
- GeoLocationId.Country((take(1) + takeLast(1)).lowercase())
-
-private fun String.generateCityCode(countryCode: GeoLocationId.Country) =
- GeoLocationId.City(countryCode, take(CITY_CODE_LENGTH).lowercase())
-
-private fun generateHostname(city: GeoLocationId.City, index: Int) =
- "${city.country.code}-${city.code}-wg-${index+1}"
-
-private const val CITY_CODE_LENGTH = 3
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt
deleted file mode 100644
index fcb1fdd194..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemStatusCellPreviewParameterProvider.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.mullvad.mullvadvpn.compose.preview
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-
-class RelayItemStatusCellPreviewParameterProvider :
- PreviewParameterProvider<List<RelayItem.Location.Country>> {
- override val values =
- sequenceOf(
- listOf(
- generateRelayItemCountry(
- name = "Relay country Active",
- cityNames = listOf("Relay city 1", "Relay city 2"),
- relaysPerCity = 2,
- ),
- generateRelayItemCountry(
- name = "Not Enabled Relay country",
- cityNames = listOf("Not Enabled city"),
- relaysPerCity = 1,
- active = false,
- ),
- generateRelayItemCountry(
- name = "Relay country Expanded",
- cityNames = listOf("Normal city"),
- relaysPerCity = 2,
- ),
- generateRelayItemCountry(
- name = "Country and city Expanded",
- cityNames = listOf("Expanded city A", "Expanded city B"),
- relaysPerCity = 2,
- ),
- )
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt
deleted file mode 100644
index aaf5fd5a6e..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayListItemPreviewData.kt
+++ /dev/null
@@ -1,120 +0,0 @@
-package net.mullvad.mullvadvpn.compose.preview
-
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
-import net.mullvad.mullvadvpn.compose.state.RelayListItemState
-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.RelayItem
-
-object RelayListItemPreviewData {
- @Suppress("LongMethod")
- fun generateRelayListItems(
- includeCustomLists: Boolean,
- isSearching: Boolean,
- ): List<RelayListItem> = buildList {
- if (!isSearching || includeCustomLists) {
- add(RelayListItem.CustomListHeader)
- // Add custom list items
- if (includeCustomLists) {
- RelayListItem.CustomListItem(
- item =
- RelayItem.CustomList(
- customList =
- CustomList(
- id = CustomListId("custom_list_id"),
- name = CustomListName.fromString("Custom List"),
- locations = emptyList(),
- ),
- locations =
- listOf(
- generateRelayItemCountry(
- name = "Country",
- cityNames = listOf("City"),
- relaysPerCity = 2,
- active = true,
- )
- ),
- ),
- isSelected = false,
- state = null,
- expanded = false,
- )
- }
- if (!isSearching) {
- add(RelayListItem.CustomListFooter(hasCustomList = includeCustomLists))
- }
- }
- add(RelayListItem.LocationHeader)
- val locations =
- listOf(
- generateRelayItemCountry(
- name = "First Country",
- cityNames = listOf("Capital City", "Minor City"),
- relaysPerCity = 2,
- active = true,
- ),
- generateRelayItemCountry(
- name = "Second Country",
- cityNames = listOf("Medium City", "Small City", "Vivec City"),
- relaysPerCity = 1,
- active = false,
- ),
- )
- addAll(
- listOf(
- RelayListItem.GeoLocationItem(
- item = locations[0],
- isSelected = false,
- depth = 0,
- expanded = true,
- state = null,
- ),
- RelayListItem.GeoLocationItem(
- item = locations[0].cities[0],
- isSelected = true,
- depth = 1,
- expanded = false,
- state = null,
- ),
- RelayListItem.GeoLocationItem(
- item = locations[0].cities[1],
- isSelected = false,
- depth = 1,
- expanded = true,
- state = null,
- ),
- RelayListItem.GeoLocationItem(
- item = locations[0].cities[1].relays[0],
- isSelected = false,
- depth = 2,
- expanded = false,
- state = RelayListItemState.USED_AS_EXIT,
- ),
- RelayListItem.GeoLocationItem(
- item = locations[0].cities[1].relays[1],
- isSelected = false,
- depth = 2,
- expanded = false,
- state = null,
- ),
- RelayListItem.GeoLocationItem(
- item = locations[1],
- isSelected = false,
- depth = 0,
- expanded = false,
- state = null,
- ),
- )
- )
- }
-
- fun generateEmptyList(searchTerm: String, isSearching: Boolean) =
- listOf(
- if (isSearching) {
- RelayListItem.LocationsEmptyText(searchTerm)
- } else {
- RelayListItem.EmptyRelayList
- }
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SearchLocationsUiStatePreviewParameterProvider.kt
index 50cf464bb5..c868360c55 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
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.compose.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItemPreviewData
import net.mullvad.mullvadvpn.usecase.FilterChip
import net.mullvad.mullvadvpn.util.Lce
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index 878ab70d9d..82d6736c99 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -83,7 +83,6 @@ import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.ConnectionButton
import net.mullvad.mullvadvpn.compose.button.SwitchLocationButton
import net.mullvad.mullvadvpn.compose.component.ConnectionStatusText
-import net.mullvad.mullvadvpn.compose.component.ExpandChevron
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.MullvadSnackbar
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName
@@ -128,6 +127,7 @@ import net.mullvad.mullvadvpn.lib.theme.color.AlphaInvisible
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
import net.mullvad.mullvadvpn.lib.tv.NavigationDrawerTv
+import net.mullvad.mullvadvpn.lib.ui.component.ExpandChevron
import net.mullvad.mullvadvpn.lib.ui.tag.CONNECT_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.CONNECT_CARD_HEADER_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.RECONNECT_BUTTON_TEST_TAG
@@ -581,7 +581,7 @@ private fun ConnectionCardHeader(
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
ConnectionStatusText(state = state.tunnelState)
if (state.tunnelState is TunnelState.Connected) {
- ExpandChevron(isExpanded = !expanded, color = MaterialTheme.colorScheme.onSurface)
+ ExpandChevron(isExpanded = !expanded)
}
}
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 1e81231e7d..6aea279110 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
@@ -8,10 +8,9 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -33,7 +32,6 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.cell.CheckableRelayLocationCell
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
@@ -46,6 +44,7 @@ import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.dialog.info.Confirmed
import net.mullvad.mullvadvpn.compose.extensions.animateScrollAndCentralizeItem
import net.mullvad.mullvadvpn.compose.preview.CustomListLocationUiStatePreviewParameterProvider
+import net.mullvad.mullvadvpn.compose.screen.location.positionalPadding
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
import net.mullvad.mullvadvpn.compose.textfield.SearchTextField
@@ -57,6 +56,7 @@ import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayLocationCell
import net.mullvad.mullvadvpn.lib.ui.tag.SAVE_BUTTON_TEST_TAG
import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.CustomListLocationsSideEffect
@@ -162,6 +162,7 @@ fun CustomListLocationsScreen(
state = lazyListState,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar),
)
+ .padding(horizontal = Dimens.mediumPadding)
.fillMaxWidth(),
state = lazyListState,
) {
@@ -169,9 +170,11 @@ fun CustomListLocationsScreen(
is Lce.Loading -> {
loading()
}
+
is Lce.Error -> {
empty()
}
+
is Lce.Content -> {
content(
uiState = state.content.value,
@@ -231,32 +234,15 @@ private fun LazyListScope.content(
LocationsEmptyText(searchTerm = uiState.searchTerm)
}
} else {
- itemsIndexed(uiState.locations, key = { index, listItem -> listItem.item.id }) {
- index,
- listItem ->
- Column(modifier = Modifier.animateItem()) {
- if (index != 0) {
- HorizontalDivider()
- }
- CheckableRelayLocationCell(
- item = listItem.item,
- onRelayCheckedChange = { isChecked ->
- onRelaySelectedChanged(listItem.item, isChecked)
- },
- checked = listItem.checked,
- depth = listItem.depth,
- onExpand = { expand -> onExpand(listItem.item, expand) },
- expanded = listItem.expanded,
- )
- }
+ items(uiState.locations, key = { listItem -> listItem.item.id }) { listItem ->
+ CheckableRelayLocationCell(
+ modifier = Modifier.animateItem().positionalPadding(listItem.itemPosition),
+ item = listItem,
+ onRelayCheckedChange = { isChecked ->
+ onRelaySelectedChanged(listItem.item, isChecked)
+ },
+ onExpand = { expand -> onExpand(listItem.item, expand) },
+ )
}
}
}
-
-private fun Lce<Boolean, CustomListLocationsUiState, Boolean>.newList(): Boolean {
- return when (this) {
- is Lce.Content -> this.value.newList
- is Lce.Loading -> this.value
- is Lce.Error -> this.error
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt
index 46013a4f1a..29870b710b 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/LocationBottomSheet.kt
@@ -30,7 +30,6 @@ import net.mullvad.mullvadvpn.compose.cell.IconCell
import net.mullvad.mullvadvpn.compose.communication.CustomListAction
import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.component.MullvadModalBottomSheet
-import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsEntryBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowEditCustomListBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowLocationBottomSheet
@@ -38,9 +37,6 @@ import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.CustomListName
import net.mullvad.mullvadvpn.lib.model.RelayItem
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaInactive
-import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_LOCATION_BOTTOM_SHEET_TEST_TAG
import net.mullvad.mullvadvpn.relaylist.canAddLocation
@@ -49,7 +45,6 @@ import net.mullvad.mullvadvpn.relaylist.canAddLocation
internal fun LocationBottomSheets(
locationBottomSheetState: LocationBottomSheetState?,
onCreateCustomList: (RelayItem.Location?) -> Unit,
- onEditCustomLists: () -> Unit,
onAddLocationToList: (RelayItem.Location, RelayItem.CustomList) -> Unit,
onRemoveLocationFromList: (location: RelayItem.Location, parent: CustomListId) -> Unit,
onEditCustomListName: (RelayItem.CustomList) -> Unit,
@@ -70,17 +65,6 @@ internal fun LocationBottomSheets(
val onBackgroundColor: Color = MaterialTheme.colorScheme.onSurface
when (locationBottomSheetState) {
- is ShowCustomListsBottomSheet -> {
- CustomListsBottomSheet(
- backgroundColor = backgroundColor,
- onBackgroundColor = onBackgroundColor,
- sheetState = sheetState,
- bottomSheetState = locationBottomSheetState,
- onCreateCustomList = { onCreateCustomList(null) },
- onEditCustomLists = onEditCustomLists,
- closeBottomSheet = onCloseBottomSheet,
- )
- }
is ShowLocationBottomSheet -> {
LocationBottomSheet(
backgroundColor = backgroundColor,
@@ -125,62 +109,6 @@ internal fun LocationBottomSheets(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-private fun CustomListsBottomSheet(
- backgroundColor: Color,
- onBackgroundColor: Color,
- sheetState: SheetState,
- bottomSheetState: ShowCustomListsBottomSheet,
- onCreateCustomList: () -> Unit,
- onEditCustomLists: () -> Unit,
- closeBottomSheet: (animate: Boolean) -> Unit,
-) {
-
- MullvadModalBottomSheet(
- sheetState = sheetState,
- backgroundColor = backgroundColor,
- onBackgroundColor = onBackgroundColor,
- onDismissRequest = { closeBottomSheet(false) },
- modifier = Modifier.testTag(SELECT_LOCATION_CUSTOM_LIST_BOTTOM_SHEET_TEST_TAG),
- ) {
- HeaderCell(
- text = stringResource(id = R.string.edit_custom_lists),
- background = backgroundColor,
- )
- HorizontalDivider(color = onBackgroundColor)
- IconCell(
- imageVector = Icons.Default.Add,
- title = stringResource(id = R.string.new_list),
- titleColor = onBackgroundColor,
- onClick = {
- onCreateCustomList()
- closeBottomSheet(true)
- },
- background = backgroundColor,
- )
- IconCell(
- imageVector = Icons.Default.Edit,
- title = stringResource(id = R.string.edit_lists),
- titleColor =
- onBackgroundColor.copy(
- alpha =
- if (bottomSheetState.editListEnabled) {
- AlphaVisible
- } else {
- AlphaInactive
- }
- ),
- onClick = {
- onEditCustomLists()
- closeBottomSheet(true)
- },
- background = backgroundColor,
- enabled = bottomSheetState.editListEnabled,
- )
- }
-}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
private fun LocationBottomSheet(
backgroundColor: Color,
onBackgroundColor: Color,
@@ -407,9 +335,6 @@ internal fun <D : DestinationSpec, R : CustomListActionResultData> ResultRecipie
}
sealed interface LocationBottomSheetState {
-
- data class ShowCustomListsBottomSheet(val editListEnabled: Boolean) : LocationBottomSheetState
-
data class ShowCustomListsEntryBottomSheet(
val customListId: CustomListId,
val customListName: CustomListName,
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 0b5617fc7e..a39270921e 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
@@ -2,105 +2,81 @@ package net.mullvad.mullvadvpn.compose.screen.location
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material3.HorizontalDivider
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.compose.cell.HeaderCell
-import net.mullvad.mullvadvpn.compose.cell.StatusRelayItemCell
import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell
-import net.mullvad.mullvadvpn.compose.cell.ThreeDotCell
import net.mullvad.mullvadvpn.compose.component.EmptyRelayListText
import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText
-import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowCustomListsEntryBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowEditCustomListBottomSheet
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState.ShowLocationBottomSheet
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.lib.model.CustomListId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.theme.Dimens
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.SelectableRelayListItem
+import net.mullvad.mullvadvpn.lib.ui.designsystem.RelayListHeader
import net.mullvad.mullvadvpn.lib.ui.tag.LOCATION_CELL_TEST_TAG
import net.mullvad.mullvadvpn.lib.ui.tag.SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG
/** Used by both the select location screen and search select location screen */
fun LazyListScope.relayListContent(
- backgroundColor: Color,
relayListItems: List<RelayListItem>,
customLists: List<RelayItem.CustomList>,
onSelectRelay: (RelayItem) -> Unit,
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
- customListHeader: @Composable LazyItemScope.() -> Unit = {
- CustomListHeader(
- onShowCustomListBottomSheet = {
- onUpdateBottomSheetState(
- ShowCustomListsBottomSheet(editListEnabled = customLists.isNotEmpty())
- )
- }
- )
- },
- locationHeader: @Composable LazyItemScope.() -> Unit = { RelayLocationHeader() },
+ customListHeader: @Composable (LazyItemScope.() -> Unit) = {},
+ locationHeader: @Composable (LazyItemScope.() -> Unit) = { RelayLocationHeader() },
) {
- itemsIndexed(
+ items(
items = relayListItems,
- key = { _: Int, item: RelayListItem -> item.key },
- contentType = { _, item -> item.contentType },
- itemContent = { index: Int, listItem: RelayListItem ->
+ key = { item: RelayListItem -> item.key },
+ contentType = { item: RelayListItem -> item.contentType },
+ itemContent = { listItem: RelayListItem ->
Column(modifier = Modifier.animateItem()) {
- if (index != 0) {
- HorizontalDivider(color = backgroundColor)
- }
when (listItem) {
RelayListItem.CustomListHeader -> customListHeader()
is RelayListItem.CustomListItem ->
CustomListItem(
listItem,
- onSelectRelay,
- { onUpdateBottomSheetState(ShowEditCustomListBottomSheet(it)) },
- { customListId, expand -> onToggleExpand(customListId, null, expand) },
+ onSelectRelay = onSelectRelay,
+ onToggleExpand = onToggleExpand,
+ onUpdateBottomSheetState = onUpdateBottomSheetState,
)
is RelayListItem.CustomListEntryItem ->
CustomListEntryItem(
listItem,
- { onSelectRelay(listItem.item) },
- // Only direct children can be removed
- if (listItem.depth == 1) {
- {
- onUpdateBottomSheetState(
- ShowCustomListsEntryBottomSheet(
- listItem.parentId,
- listItem.parentName,
- listItem.item,
- )
- )
- }
- } else {
- null
- },
- { expand: Boolean ->
- onToggleExpand(listItem.item.id, listItem.parentId, expand)
- },
+ onSelectRelay = onSelectRelay,
+ onToggleExpand = onToggleExpand,
+ onUpdateBottomSheetState = onUpdateBottomSheetState,
)
is RelayListItem.CustomListFooter -> CustomListFooter(listItem)
RelayListItem.LocationHeader -> locationHeader()
is RelayListItem.GeoLocationItem ->
- RelayLocationItem(
+ GeoLocationItem(
listItem,
- { onSelectRelay(listItem.item) },
- {
- onUpdateBottomSheetState(
- ShowLocationBottomSheet(customLists, listItem.item)
- )
- },
- { expand -> onToggleExpand(listItem.item.id, null, expand) },
+ onSelectRelay = onSelectRelay,
+ onToggleExpand = onToggleExpand,
+ onUpdateBottomSheetState = onUpdateBottomSheetState,
+ customLists = customLists,
)
is RelayListItem.LocationsEmptyText -> LocationsEmptyText(listItem.searchTerm)
is RelayListItem.EmptyRelayList -> EmptyRelayListText()
@@ -111,76 +87,107 @@ fun LazyListScope.relayListContent(
}
@Composable
-private fun LazyItemScope.RelayLocationItem(
- relayItem: RelayListItem.GeoLocationItem,
- onSelectRelay: () -> Unit,
- onLongClick: () -> Unit,
- onExpand: (Boolean) -> Unit,
+fun Modifier.positionalPadding(itemPosition: ItemPosition): Modifier =
+ when (itemPosition) {
+ ItemPosition.Top,
+ ItemPosition.Single -> padding(top = Dimens.miniPadding)
+ ItemPosition.Middle -> padding(top = Dimens.listItemDivider)
+ ItemPosition.Bottom -> padding(top = Dimens.listItemDivider, bottom = Dimens.miniPadding)
+ }
+
+@Composable
+private fun GeoLocationItem(
+ listItem: RelayListItem.GeoLocationItem,
+ onSelectRelay: (RelayItem) -> Unit,
+ onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
+ onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
+ customLists: List<RelayItem.CustomList>,
) {
- val location = relayItem.item
- StatusRelayItemCell(
- item = location,
- state = relayItem.state,
- isSelected = relayItem.isSelected,
- onClick = { onSelectRelay() },
- onLongClick = { onLongClick() },
- onToggleExpand = { onExpand(it) },
- isExpanded = relayItem.expanded,
- depth = relayItem.depth,
- modifier = Modifier.testTag(LOCATION_CELL_TEST_TAG),
+ SelectableRelayListItem(
+ relayListItem = listItem,
+ onClick = { onSelectRelay(listItem.item) },
+ onLongClick = {
+ onUpdateBottomSheetState(ShowLocationBottomSheet(customLists, listItem.item))
+ },
+ onToggleExpand = { onToggleExpand(listItem.item.id, null, it) },
+ modifier = Modifier.positionalPadding(listItem.itemPosition).testTag(LOCATION_CELL_TEST_TAG),
)
}
@Composable
-private fun LazyItemScope.CustomListEntryItem(
- itemState: RelayListItem.CustomListEntryItem,
- onSelectRelay: () -> Unit,
- onShowEditCustomListEntryBottomSheet: (() -> Unit)?,
- onToggleExpand: (Boolean) -> Unit,
+private fun CustomListItem(
+ listItem: RelayListItem.CustomListItem,
+ onSelectRelay: (RelayItem) -> Unit,
+ onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
+ onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
- val customListEntryItem = itemState.item
- StatusRelayItemCell(
- item = customListEntryItem,
- state = itemState.state,
- isSelected = false,
- onClick = onSelectRelay,
- onLongClick = onShowEditCustomListEntryBottomSheet,
- onToggleExpand = onToggleExpand,
- isExpanded = itemState.expanded,
- depth = itemState.depth,
+ SelectableRelayListItem(
+ relayListItem = listItem,
+ onClick = { onSelectRelay(listItem.item) },
+ onLongClick = { onUpdateBottomSheetState(ShowEditCustomListBottomSheet(listItem.item)) },
+ onToggleExpand = { onToggleExpand(listItem.item.id, null, it) },
+ modifier = Modifier.positionalPadding(listItem.itemPosition),
)
}
@Composable
-private fun LazyItemScope.CustomListItem(
- itemState: RelayListItem.CustomListItem,
- onSelectRelay: (item: RelayItem) -> Unit,
- onShowEditBottomSheet: (RelayItem.CustomList) -> Unit,
- onExpand: ((CustomListId, Boolean) -> Unit),
+private fun CustomListEntryItem(
+ listItem: RelayListItem.CustomListEntryItem,
+ onSelectRelay: (RelayItem) -> Unit,
+ onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
+ onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
- val customListItem = itemState.item
- StatusRelayItemCell(
- item = customListItem,
- state = itemState.state,
- isSelected = itemState.isSelected,
- onClick = { onSelectRelay(customListItem) },
- onLongClick = { onShowEditBottomSheet(customListItem) },
- onToggleExpand = { onExpand(customListItem.id, it) },
- isExpanded = itemState.expanded,
+ SelectableRelayListItem(
+ relayListItem = listItem,
+ onClick = { onSelectRelay(listItem.item) },
+ // Only direct children can be removed
+ onLongClick =
+ if (listItem.depth == 1) {
+ {
+ onUpdateBottomSheetState(
+ ShowCustomListsEntryBottomSheet(
+ listItem.parentId,
+ listItem.parentName,
+ listItem.item,
+ )
+ )
+ }
+ } else {
+ null
+ },
+ onToggleExpand = { expand: Boolean ->
+ onToggleExpand(listItem.item.id, listItem.parentId, expand)
+ },
+ modifier = Modifier.positionalPadding(listItem.itemPosition),
)
}
@Composable
-private fun LazyItemScope.CustomListHeader(onShowCustomListBottomSheet: () -> Unit) {
- ThreeDotCell(
- text = stringResource(R.string.custom_lists),
- onClickDots = onShowCustomListBottomSheet,
+fun CustomListHeader(addCustomList: () -> Unit, editCustomLists: (() -> Unit)?) {
+ RelayListHeader(
+ { Text(stringResource(R.string.custom_lists), overflow = TextOverflow.Ellipsis) },
+ actions = {
+ IconButton(onClick = addCustomList) {
+ Icon(
+ imageVector = Icons.Default.Add,
+ contentDescription = stringResource(id = R.string.new_list),
+ )
+ }
+ editCustomLists?.run {
+ IconButton(onClick = editCustomLists) {
+ Icon(
+ imageVector = Icons.Default.Edit,
+ contentDescription = stringResource(id = R.string.edit_lists),
+ )
+ }
+ }
+ },
modifier = Modifier.testTag(SELECT_LOCATION_CUSTOM_LIST_HEADER_TEST_TAG),
)
}
@Composable
-private fun LazyItemScope.CustomListFooter(item: RelayListItem.CustomListFooter) {
+private fun CustomListFooter(item: RelayListItem.CustomListFooter) {
SwitchComposeSubtitleCell(
text =
if (item.hasCustomList) {
@@ -193,6 +200,10 @@ private fun LazyItemScope.CustomListFooter(item: RelayListItem.CustomListFooter)
}
@Composable
-private fun LazyItemScope.RelayLocationHeader() {
- HeaderCell(text = stringResource(R.string.all_locations))
+private fun RelayLocationHeader() {
+ RelayListHeader(
+ content = {
+ Text(text = stringResource(R.string.all_locations), overflow = TextOverflow.Ellipsis)
+ }
+ )
}
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 f095160107..ea88ada52b 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
@@ -45,7 +45,6 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.CreateCustomListDestination
import com.ramcosta.composedestinations.generated.destinations.CustomListLocationsDestination
-import com.ramcosta.composedestinations.generated.destinations.CustomListsDestination
import com.ramcosta.composedestinations.generated.destinations.DeleteCustomListDestination
import com.ramcosta.composedestinations.generated.destinations.EditCustomListNameDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -93,7 +92,6 @@ private fun PreviewSearchLocationScreen(
{ _, _, _ -> },
{},
{},
- {},
{ _, _ -> },
{ _, _ -> },
{},
@@ -150,6 +148,15 @@ fun SearchLocation(
message = context.getString(R.string.error_occurred)
)
}
+
+ is SearchLocationSideEffect.RelayItemInactive -> {
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(R.string.relayitem_is_inactive, it.relayItem.name)
+ )
+ }
+ }
}
}
@@ -183,7 +190,6 @@ fun SearchLocation(
dropUnlessResumed { relayItem ->
navigator.navigate(CreateCustomListDestination(locationCode = relayItem?.id))
},
- onEditCustomLists = dropUnlessResumed { navigator.navigate(CustomListsDestination()) },
onAddLocationToList = viewModel::addLocationToList,
onRemoveLocationFromList = viewModel::removeLocationFromList,
onEditCustomListName =
@@ -226,7 +232,6 @@ fun SearchLocationScreen(
onToggleExpand: (RelayItemId, CustomListId?, Boolean) -> Unit,
onSearchInputChanged: (String) -> Unit,
onCreateCustomList: (location: RelayItem.Location?) -> Unit,
- onEditCustomLists: () -> Unit,
onAddLocationToList: (location: RelayItem.Location, customList: RelayItem.CustomList) -> Unit,
onRemoveLocationFromList: (location: RelayItem.Location, customListId: CustomListId) -> Unit,
onEditCustomListName: (RelayItem.CustomList) -> Unit,
@@ -251,7 +256,6 @@ fun SearchLocationScreen(
LocationBottomSheets(
locationBottomSheetState = locationBottomSheetState,
onCreateCustomList = onCreateCustomList,
- onEditCustomLists = onEditCustomLists,
onAddLocationToList = onAddLocationToList,
onRemoveLocationFromList = onRemoveLocationFromList,
onEditCustomListName = onEditCustomListName,
@@ -277,6 +281,7 @@ fun SearchLocationScreen(
LazyColumn(
modifier =
Modifier.fillMaxSize()
+ .padding(horizontal = Dimens.mediumPadding)
.background(color = backgroundColor)
.drawVerticalScrollbar(
lazyListState,
@@ -301,9 +306,8 @@ fun SearchLocationScreen(
}
is Lce.Content -> {
relayListContent(
- backgroundColor = backgroundColor,
- customLists = state.value.customLists,
relayListItems = state.value.relayListItems,
+ customLists = state.value.customLists,
onSelectRelay = onSelectRelay,
onToggleExpand = onToggleExpand,
onUpdateBottomSheetState = { 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 bf3db9a534..4ef79723c0 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
@@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.text.style.TextAlign
@@ -26,13 +25,13 @@ import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicator
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.extensions.animateScrollAndCentralizeItem
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.compose.util.RunOnKeyChange
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.util.Lce
import net.mullvad.mullvadvpn.viewmodel.location.SelectLocationListViewModel
import org.koin.androidx.compose.koinViewModel
@@ -44,10 +43,11 @@ private typealias Content = Lce.Content<SelectLocationListUiState>
@Composable
fun SelectLocationList(
- backgroundColor: Color,
relayListType: RelayListType,
onSelectRelay: (RelayItem) -> Unit,
openDaitaSettings: () -> Unit,
+ onAddCustomList: () -> Unit,
+ onEditCustomLists: (() -> Unit)?,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
val viewModel =
@@ -67,6 +67,7 @@ fun SelectLocationList(
LazyColumn(
modifier =
Modifier.fillMaxSize()
+ .padding(horizontal = Dimens.mediumPadding)
.drawVerticalScrollbar(
lazyListState,
MaterialTheme.colorScheme.onSurface.copy(alpha = AlphaScrollbar),
@@ -89,12 +90,18 @@ fun SelectLocationList(
}
is Content -> {
relayListContent(
- backgroundColor = backgroundColor,
relayListItems = stateActual.value.relayListItems,
customLists = stateActual.value.customLists,
onSelectRelay = onSelectRelay,
onToggleExpand = viewModel::onToggleExpand,
onUpdateBottomSheetState = onUpdateBottomSheetState,
+ customListHeader = {
+ CustomListHeader(
+ onAddCustomList,
+ if (stateActual.value.customLists.isNotEmpty()) onEditCustomLists
+ else null,
+ )
+ },
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/location/SelectLocationScreen.kt
index ec8c25207b..7429b39324 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
@@ -31,7 +31,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
@@ -152,6 +151,14 @@ fun SelectLocation(
message = context.getString(R.string.error_occurred)
)
}
+
+ is SelectLocationSideEffect.RelayItemInactive ->
+ launch {
+ snackbarHostState.showSnackbarImmediately(
+ message =
+ context.getString(R.string.relayitem_is_inactive, it.relayItem.name)
+ )
+ }
}
}
@@ -295,7 +302,6 @@ fun SelectLocationScreen(
LocationBottomSheets(
locationBottomSheetState = locationBottomSheetState,
onCreateCustomList = onCreateCustomList,
- onEditCustomLists = onEditCustomLists,
onAddLocationToList = onAddLocationToList,
onRemoveLocationFromList = onRemoveLocationFromList,
onEditCustomListName = onEditCustomListName,
@@ -341,9 +347,10 @@ fun SelectLocationScreen(
RelayLists(
state = state.value,
- backgroundColor = backgroundColor,
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
+ onAddCustomList = { onCreateCustomList(null) },
+ onEditCustomLists = onEditCustomLists,
onUpdateBottomSheetState = { newState ->
locationBottomSheetState = newState
},
@@ -381,9 +388,10 @@ private fun MultihopBar(relayListType: RelayListType, onSelectRelayList: (RelayL
@Composable
private fun RelayLists(
state: SelectLocationUiState,
- backgroundColor: Color,
onSelectRelay: (RelayItem) -> Unit,
openDaitaSettings: () -> Unit,
+ onAddCustomList: () -> Unit,
+ onEditCustomLists: (() -> Unit)?,
onUpdateBottomSheetState: (LocationBottomSheetState) -> Unit,
) {
// This is a workaround for the HorizontalPager being broken on Android TV when it contains
@@ -392,10 +400,11 @@ private fun RelayLists(
if (configuration.navigation == Configuration.NAVIGATION_DPAD) {
SelectLocationList(
- backgroundColor = backgroundColor,
relayListType = state.relayListType,
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
+ onAddCustomList = onAddCustomList,
+ onEditCustomLists = onEditCustomLists,
onUpdateBottomSheetState = onUpdateBottomSheetState,
)
} else {
@@ -420,10 +429,11 @@ private fun RelayLists(
},
) { pageIndex ->
SelectLocationList(
- backgroundColor = backgroundColor,
relayListType = RelayListType.entries[pageIndex],
onSelectRelay = onSelectRelay,
openDaitaSettings = openDaitaSettings,
+ onAddCustomList = onAddCustomList,
+ onEditCustomLists = onEditCustomLists,
onUpdateBottomSheetState = onUpdateBottomSheetState,
)
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
index 9f8eae8c53..527bad85fa 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/CustomListLocationsUiState.kt
@@ -1,6 +1,6 @@
package net.mullvad.mullvadvpn.compose.state
-import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
import net.mullvad.mullvadvpn.util.Lce
data class CustomListLocationsUiState(
@@ -12,12 +12,5 @@ data class CustomListLocationsData(
val saveEnabled: Boolean,
val hasUnsavedChanges: Boolean,
val searchTerm: String,
- val locations: List<RelayLocationListItem>,
-)
-
-data class RelayLocationListItem(
- val item: RelayItem.Location,
- val depth: Int = 0,
- val checked: Boolean = false,
- val expanded: Boolean = false,
+ val locations: List<CheckableRelayListItem>,
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt
deleted file mode 100644
index 028912f15d..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/RelayListItem.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package net.mullvad.mullvadvpn.compose.state
-
-import net.mullvad.mullvadvpn.lib.model.CustomListId
-import net.mullvad.mullvadvpn.lib.model.CustomListName
-import net.mullvad.mullvadvpn.lib.model.RelayItem
-
-enum class RelayListItemContentType {
- CUSTOM_LIST_HEADER,
- CUSTOM_LIST_ITEM,
- CUSTOM_LIST_ENTRY_ITEM,
- CUSTOM_LIST_FOOTER,
- LOCATION_HEADER,
- LOCATION_ITEM,
- LOCATIONS_EMPTY_TEXT,
- EMPTY_RELAY_LIST,
-}
-
-enum class RelayListItemState {
- USED_AS_ENTRY,
- USED_AS_EXIT,
-}
-
-sealed interface RelayListItem {
- val key: Any
- val contentType: RelayListItemContentType
-
- data object CustomListHeader : RelayListItem {
- override val key = "custom_list_header"
- override val contentType = RelayListItemContentType.CUSTOM_LIST_HEADER
- }
-
- sealed interface SelectableItem : RelayListItem {
- val depth: Int
- val isSelected: Boolean
- val expanded: Boolean
- val state: RelayListItemState?
- }
-
- data class CustomListItem(
- val item: RelayItem.CustomList,
- override val isSelected: Boolean = false,
- override val expanded: Boolean = false,
- override val state: RelayListItemState? = null,
- ) : SelectableItem {
- override val key = item.id
- override val depth: Int = 0
- override val contentType = RelayListItemContentType.CUSTOM_LIST_ITEM
- }
-
- data class CustomListEntryItem(
- val parentId: CustomListId,
- val parentName: CustomListName,
- val item: RelayItem.Location,
- override val expanded: Boolean,
- override val depth: Int = 0,
- override val state: RelayListItemState? = null,
- ) : SelectableItem {
- override val key = parentId to item.id
-
- // Can't be displayed as selected
- override val isSelected: Boolean = false
- override val contentType = RelayListItemContentType.CUSTOM_LIST_ENTRY_ITEM
- }
-
- data class CustomListFooter(val hasCustomList: Boolean) : RelayListItem {
- override val key = "custom_list_footer"
- override val contentType = RelayListItemContentType.CUSTOM_LIST_FOOTER
- }
-
- data object LocationHeader : RelayListItem {
- override val key = "location_header"
- override val contentType = RelayListItemContentType.LOCATION_HEADER
- }
-
- data class GeoLocationItem(
- val item: RelayItem.Location,
- override val isSelected: Boolean = false,
- override val depth: Int = 0,
- override val expanded: Boolean = false,
- override val state: RelayListItemState? = null,
- ) : SelectableItem {
- override val key = item.id
- override val contentType = RelayListItemContentType.LOCATION_ITEM
- }
-
- data class LocationsEmptyText(val searchTerm: String) : RelayListItem {
- override val key = "locations_empty_text"
- override val contentType = RelayListItemContentType.LOCATIONS_EMPTY_TEXT
- }
-
- data object EmptyRelayList : RelayListItem {
- override val key = "empty_relay_list"
- override val contentType = RelayListItemContentType.EMPTY_RELAY_LIST
- }
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/SearchLocationUiState.kt
index c377be8814..909e4ea8eb 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
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.usecase.FilterChip
data class SearchLocationUiState(
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 393286a35e..39199b9d04 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
@@ -1,6 +1,7 @@
package net.mullvad.mullvadvpn.compose.state
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
data class SelectLocationListUiState(
val relayListItems: List<RelayListItem>,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
index baadf379cb..8cee6c7423 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModel.kt
@@ -21,9 +21,10 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData
import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
import net.mullvad.mullvadvpn.relaylist.ancestors
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.newFilterOnSearch
@@ -62,11 +63,13 @@ class CustomListLocationsViewModel(
newList = navArgs.newList,
content = Lce.Loading(Unit),
)
+
relayCountries.isEmpty() ->
CustomListLocationsUiState(
newList = navArgs.newList,
content = Lce.Error(Unit),
)
+
else -> {
val (expandSet, filteredRelayCountries) =
searchRelayListLocations(searchTerm, relayCountries)
@@ -78,10 +81,13 @@ class CustomListLocationsViewModel(
CustomListLocationsData(
searchTerm = searchTerm,
locations =
- filteredRelayCountries.toRelayItems(
- isSelected = { it in selectedLocations },
- isExpanded = { it in expandedLocations },
- ),
+ filteredRelayCountries.flatMap {
+ it.toRelayItems(
+ isSelected = { it in selectedLocations },
+ isExpanded = { it in expandedLocations },
+ isLastChild = true,
+ )
+ },
saveEnabled =
selectedLocations.isNotEmpty() &&
selectedLocations != _initialLocations.value,
@@ -190,6 +196,7 @@ class CustomListLocationsViewModel(
.find { it.id == relayItem.id.country }
?.let { updateSelectionList.remove(it) }
}
+
is RelayItem.Location.Relay -> {
availableLocations
.flatMap { country -> country.cities }
@@ -199,6 +206,7 @@ class CustomListLocationsViewModel(
.find { it.id == relayItem.id.country }
?.let { updateSelectionList.remove(it) }
}
+
is RelayItem.Location.Country -> {
/* Do nothing */
}
@@ -216,9 +224,11 @@ class CustomListLocationsViewModel(
saveSelectionList.removeAll(relayItem.cities)
saveSelectionList.removeAll(relayItem.relays)
}
+
is RelayItem.Location.City -> {
saveSelectionList.removeAll(relayItem.relays)
}
+
is RelayItem.Location.Relay -> {
/* Do nothing */
}
@@ -240,42 +250,56 @@ class CustomListLocationsViewModel(
private fun initialExpands(locations: List<RelayItem.Location>): Set<RelayItemId> =
locations.flatMap { it.id.ancestors() }.toSet()
- private fun List<RelayItem.Location>.toRelayItems(
+ private fun RelayItem.Location.toRelayItems(
isSelected: (RelayItem) -> Boolean,
isExpanded: (RelayItemId) -> Boolean,
depth: Int = 0,
- ): List<RelayLocationListItem> = flatMap { relayItem ->
- buildList {
- val expanded = isExpanded(relayItem.id)
- add(
- RelayLocationListItem(
- item = relayItem,
- depth = depth,
- checked = isSelected(relayItem),
- expanded = expanded,
- )
+ isLastChild: Boolean,
+ ): List<CheckableRelayListItem> = buildList {
+ val expanded = isExpanded(id)
+ add(
+ CheckableRelayListItem(
+ item = this@toRelayItems,
+ depth = depth,
+ checked = isSelected(this@toRelayItems),
+ expanded = expanded,
+ itemPosition =
+ when {
+ this@toRelayItems is RelayItem.Location.Country ->
+ if (!expanded) ItemPosition.Single else ItemPosition.Top
+ isLastChild && !expanded -> ItemPosition.Bottom
+ else -> ItemPosition.Middle
+ },
)
- if (expanded) {
- when (relayItem) {
- is RelayItem.Location.City ->
- addAll(
- relayItem.relays.toRelayItems(
+ )
+ if (expanded) {
+ when (this@toRelayItems) {
+ is RelayItem.Location.City ->
+ addAll(
+ relays.flatMapIndexed { index, relay ->
+ relay.toRelayItems(
isSelected = isSelected,
isExpanded = isExpanded,
depth = depth + 1,
+ isLastChild = isLastChild && index == relays.lastIndex,
)
- )
- is RelayItem.Location.Country ->
- addAll(
- relayItem.cities.toRelayItems(
+ }
+ )
+
+ is RelayItem.Location.Country ->
+ addAll(
+ cities.flatMapIndexed { index, item ->
+ item.toRelayItems(
isSelected = isSelected,
isExpanded = isExpanded,
depth = depth + 1,
+ isLastChild = isLastChild && index == cities.lastIndex,
)
- )
- is RelayItem.Location.Relay -> {
- /* Do nothing */
- }
+ }
+ )
+
+ is RelayItem.Location.Relay -> {
+ /* Do nothing */
}
}
}
@@ -299,6 +323,7 @@ class CustomListLocationsViewModel(
relayListRepository.find(success.addedLocations.first())!!.name,
undo = success.undo,
)
+
success.removedLocations.size == 1 && success.addedLocations.isEmpty() ->
CustomListActionResultData.Success.LocationRemoved(
customListName = success.name,
@@ -306,6 +331,7 @@ class CustomListLocationsViewModel(
relayListRepository.find(success.removedLocations.first())!!.name,
undo = success.undo,
)
+
else ->
CustomListActionResultData.Success.LocationChanged(
customListName = success.name,
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 d80b8fc548..85bd7b282f 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,13 +1,14 @@
package net.mullvad.mullvadvpn.viewmodel.location
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
-import net.mullvad.mullvadvpn.compose.state.RelayListItemState
import net.mullvad.mullvadvpn.compose.state.RelayListType
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.RelayItemSelection
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.ItemPosition
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItemState
import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm
// Creates a relay list to be displayed by RelayListContent
@@ -176,19 +177,26 @@ private fun createCustomListRelayItems(
selectedByOtherId = selectedByOtherEntryExitList,
),
expanded = expanded,
+ itemPosition =
+ if (expanded) {
+ ItemPosition.Top
+ } else {
+ ItemPosition.Single
+ },
)
)
if (expanded) {
addAll(
- customList.locations.flatMap {
+ customList.locations.flatMapIndexed { index, item ->
createCustomListEntry(
parent = customList,
- item = it,
+ item = item,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = 1,
isExpanded = isExpanded,
+ isLast = index == customList.locations.lastIndex,
)
}
)
@@ -205,13 +213,14 @@ private fun createLocationSection(
): List<RelayListItem> = buildList {
add(RelayListItem.LocationHeader)
addAll(
- countries.flatMap { country ->
+ countries.flatMapIndexed { index, country ->
createGeoLocationEntry(
item = country,
selectedByThisEntryExitList = selectedByThisEntryExitList,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
isExpanded = isExpanded,
+ isLast = true,
)
}
)
@@ -234,6 +243,7 @@ private fun createLocationSectionSearching(
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
isExpanded = isExpanded,
+ isLast = true,
)
}
)
@@ -247,6 +257,7 @@ private fun createCustomListEntry(
selectedByOtherEntryExitList: RelayItemId?,
depth: Int = 1,
isExpanded: (String) -> Boolean,
+ isLast: Boolean,
): List<RelayListItem.CustomListEntryItem> = buildList {
val expanded = isExpanded(item.id.expandKey(parent.id))
add(
@@ -261,6 +272,12 @@ private fun createCustomListEntry(
),
expanded = expanded,
depth = depth,
+ itemPosition =
+ if (!expanded && isLast) {
+ ItemPosition.Bottom
+ } else {
+ ItemPosition.Middle
+ },
)
)
@@ -268,27 +285,29 @@ private fun createCustomListEntry(
when (item) {
is RelayItem.Location.City ->
addAll(
- item.relays.flatMap {
+ item.relays.flatMapIndexed { index, relay ->
createCustomListEntry(
parent = parent,
- item = it,
+ item = relay,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.relays.lastIndex,
)
}
)
is RelayItem.Location.Country ->
addAll(
- item.cities.flatMap {
+ item.cities.flatMapIndexed { index, city ->
createCustomListEntry(
parent = parent,
- item = it,
+ item = city,
relayListType = relayListType,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.cities.lastIndex,
)
}
)
@@ -304,6 +323,7 @@ private fun createGeoLocationEntry(
selectedByOtherEntryExitList: RelayItemId?,
depth: Int = 0,
isExpanded: (String) -> Boolean,
+ isLast: Boolean,
): List<RelayListItem.GeoLocationItem> = buildList {
val expanded = isExpanded(item.id.expandKey())
@@ -318,6 +338,24 @@ private fun createGeoLocationEntry(
),
depth = depth,
expanded = expanded,
+ itemPosition =
+ when (item) {
+ is RelayItem.Location.Country -> {
+ if (expanded) {
+ ItemPosition.Top
+ } else {
+ ItemPosition.Single
+ }
+ }
+
+ else -> {
+ if (isLast && !expanded) {
+ ItemPosition.Bottom
+ } else {
+ ItemPosition.Middle
+ }
+ }
+ },
)
)
@@ -325,27 +363,29 @@ private fun createGeoLocationEntry(
when (item) {
is RelayItem.Location.City ->
addAll(
- item.relays.flatMap {
+ item.relays.flatMapIndexed { index, relay ->
createGeoLocationEntry(
- item = it,
+ item = relay,
relayListType = relayListType,
selectedByThisEntryExitList = selectedByThisEntryExitList,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.relays.lastIndex,
)
}
)
is RelayItem.Location.Country ->
addAll(
- item.cities.flatMap {
+ item.cities.flatMapIndexed { index, city ->
createGeoLocationEntry(
- item = it,
+ item = city,
relayListType = relayListType,
selectedByThisEntryExitList = selectedByThisEntryExitList,
selectedByOtherEntryExitList = selectedByOtherEntryExitList,
depth = depth + 1,
isExpanded = isExpanded,
+ isLast = isLast && index == item.cities.lastIndex,
)
}
)
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 5310fe5ca8..9ac657423f 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
@@ -119,16 +119,24 @@ class SearchLocationViewModel(
fun selectRelay(relayItem: RelayItem) {
viewModelScope.launch {
- selectRelayItem(
- relayItem = relayItem,
- relayListType = relayListType,
- selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
- selectExitLocation = relayListRepository::updateSelectedRelayLocation,
- )
- .fold(
- { _uiSideEffect.send(SearchLocationSideEffect.GenericError) },
- { _uiSideEffect.send(SearchLocationSideEffect.LocationSelected(relayListType)) },
- )
+ if (relayItem.active) {
+ selectRelayItem(
+ relayItem = relayItem,
+ relayListType = relayListType,
+ selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
+ selectExitLocation = relayListRepository::updateSelectedRelayLocation,
+ )
+ .fold(
+ { _uiSideEffect.send(SearchLocationSideEffect.GenericError) },
+ {
+ _uiSideEffect.send(
+ SearchLocationSideEffect.LocationSelected(relayListType)
+ )
+ },
+ )
+ } else {
+ _uiSideEffect.send(SearchLocationSideEffect.RelayItemInactive(relayItem))
+ }
}
}
@@ -217,5 +225,7 @@ sealed interface SearchLocationSideEffect {
data class CustomListActionToast(val resultData: CustomListActionResultData) :
SearchLocationSideEffect
+ data class RelayItemInactive(val relayItem: RelayItem) : SearchLocationSideEffect
+
data object GenericError : SearchLocationSideEffect
}
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 c9cca74602..0420559245 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
@@ -75,22 +75,27 @@ class SelectLocationViewModel(
fun selectRelay(relayItem: RelayItem) {
viewModelScope.launch {
- selectRelayItem(
- relayItem = relayItem,
- relayListType = _relayListType.value,
- selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
- selectExitLocation = relayListRepository::updateSelectedRelayLocation,
- )
- .fold(
- { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
- {
- when (_relayListType.value) {
- RelayListType.ENTRY -> _relayListType.emit(RelayListType.EXIT)
- RelayListType.EXIT ->
- _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
- }
- },
- )
+ if (relayItem.active) {
+
+ selectRelayItem(
+ relayItem = relayItem,
+ relayListType = _relayListType.value,
+ selectEntryLocation = wireguardConstraintsRepository::setEntryLocation,
+ selectExitLocation = relayListRepository::updateSelectedRelayLocation,
+ )
+ .fold(
+ { _uiSideEffect.send(SelectLocationSideEffect.GenericError) },
+ {
+ when (_relayListType.value) {
+ RelayListType.ENTRY -> _relayListType.emit(RelayListType.EXIT)
+ RelayListType.EXIT ->
+ _uiSideEffect.send(SelectLocationSideEffect.CloseScreen)
+ }
+ },
+ )
+ } else {
+ _uiSideEffect.send(SelectLocationSideEffect.RelayItemInactive(relayItem))
+ }
}
}
@@ -139,4 +144,6 @@ sealed interface SelectLocationSideEffect {
SelectLocationSideEffect
data object GenericError : SelectLocationSideEffect
+
+ data class RelayItemInactive(val relayItem: RelayItem) : SelectLocationSideEffect
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
index edb4a9adfe..9b31f8bf24 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/CustomListLocationsViewModelTest.kt
@@ -15,7 +15,6 @@ import net.mullvad.mullvadvpn.compose.communication.LocationsChanged
import net.mullvad.mullvadvpn.compose.screen.CustomListLocationsNavArgs
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsData
import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState
-import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.common.test.assertLists
import net.mullvad.mullvadvpn.lib.model.CustomList
@@ -25,6 +24,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.Ownership
import net.mullvad.mullvadvpn.lib.model.ProviderId
import net.mullvad.mullvadvpn.lib.model.RelayItem
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.CheckableRelayListItem
import net.mullvad.mullvadvpn.relaylist.descendants
import net.mullvad.mullvadvpn.relaylist.withDescendants
import net.mullvad.mullvadvpn.repository.RelayListRepository
@@ -77,7 +77,7 @@ class CustomListLocationsViewModelTest {
// Arrange
val expectedList =
DUMMY_COUNTRIES.map {
- RelayLocationListItem(
+ CheckableRelayListItem(
item = it,
depth = it.toDepth(),
checked = false,
@@ -118,8 +118,9 @@ class CustomListLocationsViewModelTest {
viewModel.uiState.test {
// Check no selected
val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(emptyList<RelayItem>(), firstState.content.selectedLocations())
+ val firstStateContent = firstState.content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(emptyList<RelayItem>(), firstStateContent.selectedLocations())
// Expand country
viewModel.onExpand(DUMMY_COUNTRIES[0], true)
awaitItem()
@@ -130,8 +131,9 @@ class CustomListLocationsViewModelTest {
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], true)
// Check all items selected
val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertLists(expectedSelection, secondState.content.selectedLocations())
+ val content = secondState.content
+ assertIs<Lce.Content<CustomListLocationsData>>(content)
+ assertLists(expectedSelection, content.selectedLocations())
}
}
@@ -150,14 +152,14 @@ class CustomListLocationsViewModelTest {
// Act, Assert
viewModel.uiState.test {
// Check initial selected
- val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(initialSelectionIds, firstState.content.selectedLocations())
+ val firstStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(initialSelectionIds, firstStateContent.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], false)
// Check all items selected
- val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertEquals(expectedSelection, secondState.content.selectedLocations())
+ val secondStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(secondStateContent)
+ assertEquals(expectedSelection, secondStateContent.selectedLocations())
}
}
@@ -176,14 +178,14 @@ class CustomListLocationsViewModelTest {
// Act, Assert
viewModel.uiState.test {
- val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(initialSelectionIds, firstState.content.selectedLocations())
+ val firstStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(initialSelectionIds, firstStateContent.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0], false)
// Check all items selected
- val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertEquals(expectedSelection, secondState.content.selectedLocations())
+ val secondStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(secondStateContent)
+ assertEquals(expectedSelection, secondStateContent.selectedLocations())
}
}
@@ -205,14 +207,14 @@ class CustomListLocationsViewModelTest {
// Expand city
viewModel.onExpand(DUMMY_COUNTRIES[0].cities[0], true)
// Check no selected
- val firstState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(firstState.content)
- assertEquals(emptyList<RelayItem>(), firstState.content.selectedLocations())
+ val firstStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(firstStateContent)
+ assertEquals(emptyList<RelayItem>(), firstStateContent.selectedLocations())
viewModel.onRelaySelectionClick(DUMMY_COUNTRIES[0].cities[0].relays[0], true)
// Check all items selected
- val secondState = awaitItem()
- assertIs<Lce.Content<CustomListLocationsData>>(secondState.content)
- assertEquals(expectedSelection, secondState.content.selectedLocations())
+ val secondStateContent = awaitItem().content
+ assertIs<Lce.Content<CustomListLocationsData>>(secondStateContent)
+ assertEquals(expectedSelection, secondStateContent.selectedLocations())
}
}
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 0166bafa98..ad0f87638f 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
@@ -8,7 +8,6 @@ import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.compose.screen.location.SearchLocationNavArgs
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SearchLocationUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
@@ -18,6 +17,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemSelection
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
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 46994ead49..fb974e52fb 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
@@ -6,7 +6,6 @@ import io.mockk.mockk
import kotlin.test.assertIs
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.compose.state.RelayListItem
import net.mullvad.mullvadvpn.compose.state.RelayListType
import net.mullvad.mullvadvpn.compose.state.SelectLocationListUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
@@ -16,6 +15,7 @@ import net.mullvad.mullvadvpn.lib.model.GeoLocationId
import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemSelection
import net.mullvad.mullvadvpn.lib.model.Settings
+import net.mullvad.mullvadvpn.lib.ui.component.relaylist.RelayListItem
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.repository.WireguardConstraintsRepository
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 304b664dc6..7115cd58c0 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
@@ -108,6 +108,7 @@ class SelectLocationViewModelTest {
val mockRelayItem: RelayItem.Location.Country = mockk()
val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
every { mockRelayItem.id } returns relayItemId
+ every { mockRelayItem.active } returns true
coEvery { mockRelayListRepository.updateSelectedRelayLocation(relayItemId) } returns
Unit.right()
@@ -126,6 +127,7 @@ class SelectLocationViewModelTest {
// Arrange
val mockRelayItem: RelayItem.Location.Country = mockk()
val relayItemId: GeoLocationId.Country = mockk(relaxed = true)
+ every { mockRelayItem.active } returns true
every { mockRelayItem.id } returns relayItemId
coEvery { mockWireguardConstraintsRepository.setEntryLocation(relayItemId) } returns
Unit.right()