diff options
Diffstat (limited to 'android/app/src')
33 files changed, 386 insertions, 1082 deletions
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListLocationsScreenTest.kt index 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() |
