From 0ed5b277904e4afb576d7ecf62f0d58142ddb9d4 Mon Sep 17 00:00:00 2001 From: Jonatan Rhodin Date: Mon, 22 Jul 2024 01:00:35 +0200 Subject: [PATCH] Improve and expand upon result data --- .../CustomListActionResultData.kt | 54 ++++++++ .../compose/dialog/CreateCustomListDialog.kt | 4 +- .../DeleteCustomListConfirmationDialog.kt | 4 +- .../dialog/EditCustomListNameDialog.kt | 4 +- .../screen/CustomListLocationsScreen.kt | 7 +- .../compose/screen/CustomListsScreen.kt | 6 +- .../compose/screen/EditCustomListScreen.kt | 6 +- .../compose/screen/SelectLocationScreen.kt | 93 ++++++------- .../repository/RelayListRepository.kt | 4 + .../CreateCustomListDialogViewModel.kt | 13 +- .../viewmodel/CustomListLocationsViewModel.kt | 53 +++++-- .../DeleteCustomListConfirmationViewModel.kt | 11 +- .../EditCustomListNameDialogViewModel.kt | 16 ++- .../viewmodel/SelectLocationViewModel.kt | 131 +++++++++++------- .../resource/src/main/res/values/strings.xml | 1 + 15 files changed, 274 insertions(+), 133 deletions(-) create mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListActionResultData.kt diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListActionResultData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListActionResultData.kt new file mode 100644 index 000000000000..8de9b616a797 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListActionResultData.kt @@ -0,0 +1,54 @@ +package net.mullvad.mullvadvpn.compose.communication + +import android.os.Parcelable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import net.mullvad.mullvadvpn.lib.model.CustomListName + +sealed interface CustomListActionResultData : Parcelable { + val undo: CustomListAction? + + @Parcelize + data class CreatedWithLocations( + val customListName: CustomListName, + val locationNames: List, + override val undo: CustomListAction + ) : CustomListActionResultData + + @Parcelize + data class Deleted( + val customListName: CustomListName, + override val undo: CustomListAction.Create + ) : CustomListActionResultData + + @Parcelize + data class Renamed(val newName: CustomListName, override val undo: CustomListAction) : + CustomListActionResultData + + @Parcelize + data class LocationAdded( + val customListName: CustomListName, + val locationName: String, + override val undo: CustomListAction + ) : CustomListActionResultData + + @Parcelize + data class LocationRemoved( + val customListName: CustomListName, + val locationName: String, + override val undo: CustomListAction + ) : CustomListActionResultData + + @Parcelize + data class LocationChanged( + val customListName: CustomListName, + override val undo: CustomListAction + ) : CustomListActionResultData + + @Parcelize + data object GenericError : CustomListActionResultData { + @IgnoredOnParcel override val undo: CustomListAction? = null + } + + fun hasUndo() = undo != null +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt index db7f664e37b4..cf7e5361c907 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/CreateCustomListDialog.kt @@ -23,7 +23,7 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.spec.DestinationStyle import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.PrimaryButton -import net.mullvad.mullvadvpn.compose.communication.Created +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.component.CustomListNameTextField import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState import net.mullvad.mullvadvpn.compose.test.CREATE_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG @@ -63,7 +63,7 @@ data class CreateCustomListNavArgs(val locationCode: GeoLocationId?) ) fun CreateCustomList( navigator: DestinationsNavigator, - backNavigator: ResultBackNavigator, + backNavigator: ResultBackNavigator, ) { val vm: CreateCustomListDialogViewModel = koinViewModel() LaunchedEffect(key1 = Unit) { diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt index 0f26bcbe4865..d1dc697981fb 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DeleteCustomListConfirmationDialog.kt @@ -11,7 +11,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.spec.DestinationStyle import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.compose.communication.Deleted +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState import net.mullvad.mullvadvpn.compose.util.LaunchedEffectCollect import net.mullvad.mullvadvpn.lib.model.CustomListId @@ -39,7 +39,7 @@ data class DeleteCustomListNavArgs(val customListId: CustomListId, val name: Cus navArgs = DeleteCustomListNavArgs::class ) fun DeleteCustomList( - navigator: ResultBackNavigator, + navigator: ResultBackNavigator, ) { val viewModel: DeleteCustomListConfirmationViewModel = koinViewModel() val state by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt index 3a6d234fc6b1..970fef88410b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/EditCustomListNameDialog.kt @@ -17,7 +17,7 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.spec.DestinationStyle import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.PrimaryButton -import net.mullvad.mullvadvpn.compose.communication.Renamed +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.component.CustomListNameTextField import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState import net.mullvad.mullvadvpn.compose.test.EDIT_CUSTOM_LIST_DIALOG_INPUT_TEST_TAG @@ -50,7 +50,7 @@ data class EditCustomListNameNavArgs( navArgs = EditCustomListNameNavArgs::class ) fun EditCustomListName( - backNavigator: ResultBackNavigator, + backNavigator: ResultBackNavigator, ) { val vm: EditCustomListNameDialogViewModel = koinViewModel() LaunchedEffectCollect(vm.uiSideEffect) { sideEffect -> 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 cb3767784edb..b66a3f0c453e 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 @@ -38,7 +38,7 @@ import com.ramcosta.composedestinations.result.ResultRecipient import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.CheckableRelayLocationCell -import net.mullvad.mullvadvpn.compose.communication.LocationsChanged +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton @@ -79,7 +79,7 @@ data class CustomListLocationsNavArgs( ) fun CustomListLocations( navigator: DestinationsNavigator, - backNavigator: ResultBackNavigator, + backNavigator: ResultBackNavigator, discardChangesResultRecipient: ResultRecipient, ) { val customListsViewModel = koinViewModel() @@ -99,9 +99,8 @@ fun CustomListLocations( val context: Context = LocalContext.current LaunchedEffectCollect(customListsViewModel.uiSideEffect) { sideEffect -> when (sideEffect) { - is CustomListLocationsSideEffect.ReturnWithResult -> + is CustomListLocationsSideEffect.ReturnWithResultData -> backNavigator.navigateBack(result = sideEffect.result) - CustomListLocationsSideEffect.CloseScreen -> backNavigator.navigateBack() CustomListLocationsSideEffect.Error -> launch { snackbarHostState.showSnackbarImmediately( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt index 5b9f0d4cc1c1..77ab226ea931 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/CustomListsScreen.kt @@ -34,6 +34,7 @@ import com.ramcosta.composedestinations.result.ResultRecipient import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.communication.Deleted import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton @@ -63,7 +64,8 @@ private fun PreviewCustomListsScreen() { @Destination(style = SlideInFromRightTransition::class) fun CustomLists( navigator: DestinationsNavigator, - editCustomListResultRecipient: ResultRecipient + editCustomListResultRecipient: + ResultRecipient ) { val viewModel = koinViewModel() val state by viewModel.uiState.collectAsStateWithLifecycle() @@ -82,7 +84,7 @@ fun CustomLists( message = context.getString( R.string.delete_custom_list_message, - result.value.name + result.value.customListName ), actionLabel = context.getString(R.string.undo), duration = SnackbarDuration.Long, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt index 365328ea939a..f0e9f174cba3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/EditCustomListScreen.kt @@ -33,6 +33,7 @@ import com.ramcosta.composedestinations.result.ResultBackNavigator import com.ramcosta.composedestinations.result.ResultRecipient import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.TwoRowCell +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.communication.Deleted import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton @@ -83,8 +84,9 @@ data class EditCustomListNavArgs(val customListId: CustomListId) ) fun EditCustomList( navigator: DestinationsNavigator, - backNavigator: ResultBackNavigator, - confirmDeleteListResultRecipient: ResultRecipient + backNavigator: ResultBackNavigator, + confirmDeleteListResultRecipient: + ResultRecipient ) { val viewModel = koinViewModel() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt index 2cffcdedda78..0dcdbd8aff49 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SelectLocationScreen.kt @@ -68,12 +68,8 @@ import net.mullvad.mullvadvpn.compose.cell.IconCell 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.communication.Created import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.CustomListSuccess -import net.mullvad.mullvadvpn.compose.communication.Deleted -import net.mullvad.mullvadvpn.compose.communication.LocationsChanged -import net.mullvad.mullvadvpn.compose.communication.Renamed +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.MullvadModalBottomSheet @@ -132,12 +128,17 @@ private fun PreviewSelectLocationScreen() { fun SelectLocation( navigator: DestinationsNavigator, backNavigator: ResultBackNavigator, - createCustomListDialogResultRecipient: ResultRecipient, + createCustomListDialogResultRecipient: + ResultRecipient< + CreateCustomListDestination, + CustomListActionResultData.CreatedWithLocations + >, editCustomListNameDialogResultRecipient: - ResultRecipient, - deleteCustomListDialogResultRecipient: ResultRecipient, + ResultRecipient, + deleteCustomListDialogResultRecipient: + ResultRecipient, updateCustomListResultRecipient: - ResultRecipient + ResultRecipient ) { val vm = koinViewModel() val state = vm.uiState.collectAsStateWithLifecycle() @@ -147,22 +148,12 @@ fun SelectLocation( val lazyListState = rememberLazyListState() CollectSideEffectWithLifecycle(vm.uiSideEffect) { when (it) { - SelectLocationSideEffect.CloseScreen -> { - backNavigator.navigateBack(result = true) - } - is SelectLocationSideEffect.LocationAddedToCustomList -> - launch { - snackbarHostState.showResultSnackbar( - context = context, - result = it.result, - onUndo = vm::performAction - ) - } - is SelectLocationSideEffect.LocationRemovedFromCustomList -> + SelectLocationSideEffect.CloseScreen -> backNavigator.navigateBack(result = true) + is SelectLocationSideEffect.CustomListActionToast -> launch { snackbarHostState.showResultSnackbar( context = context, - result = it.result, + result = it.resultData, onUndo = vm::performAction ) } @@ -335,7 +326,7 @@ fun SelectLocationScreen( itemsIndexed( items = state.relayListItems, - key = { index: Int, item: RelayListItem -> item.key }, + key = { _: Int, item: RelayListItem -> item.key }, contentType = { _, item -> item.contentType }, itemContent = { index: Int, listItem: RelayListItem -> Column(modifier = Modifier.animateItem()) { @@ -834,45 +825,49 @@ private suspend fun LazyListState.animateScrollAndCentralizeItem(index: Int) { private suspend fun SnackbarHostState.showResultSnackbar( context: Context, - result: CustomListSuccess, + result: CustomListActionResultData, onUndo: (CustomListAction) -> Unit ) { + showSnackbarImmediately( message = result.message(context), - actionLabel = context.getString(R.string.undo), + actionLabel = + if (result.hasUndo()) context.getString(R.string.undo) + else { + null + }, duration = SnackbarDuration.Long, - onAction = { onUndo(result.undo) } + onAction = { result.undo?.let { onUndo(it) } } ) } -private fun CustomListSuccess.message(context: Context): String = +private fun CustomListActionResultData.message(context: Context): String = when (this) { - is Created -> - locationNames.firstOrNull()?.let { locationName -> - context.getString(R.string.location_was_added_to_list, locationName, name) - } ?: context.getString(R.string.locations_were_changed_for, name) - is Deleted -> context.getString(R.string.delete_custom_list_message, name) - is Renamed -> context.getString(R.string.name_was_changed_to, name) - is LocationsChanged -> - when { - addedLocations.size == 1 && removedLocations.isEmpty() -> - context.getString( - R.string.location_was_added_to_list, - addedLocations.first(), - name - ) - removedLocations.size == 1 && addedLocations.isEmpty() -> - context.getString( - R.string.location_was_removed_from_list, - removedLocations.first(), - name - ) - else -> context.getString(R.string.locations_were_changed_for, name) + is CustomListActionResultData.CreatedWithLocations -> + if (locationNames.size == 1) { + context.getString( + R.string.location_was_added_to_list, + locationNames.first(), + customListName + ) + } else { + context.getString(R.string.create_custom_list_message, customListName) } + is CustomListActionResultData.Deleted -> + context.getString(R.string.delete_custom_list_message, customListName) + is CustomListActionResultData.LocationAdded -> + context.getString(R.string.location_was_added_to_list, locationName, customListName) + is CustomListActionResultData.LocationRemoved -> + context.getString(R.string.location_was_removed_from_list, locationName, customListName) + is CustomListActionResultData.LocationChanged -> + context.getString(R.string.locations_were_changed_for, customListName) + is CustomListActionResultData.Renamed -> + context.getString(R.string.name_was_changed_to, newName) + CustomListActionResultData.GenericError -> context.getString(R.string.error_occurred) } @Composable -private fun ResultRecipient +private fun ResultRecipient .OnCustomListNavResult( snackbarHostState: SnackbarHostState, performAction: (action: CustomListAction) -> Unit diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt index 7d9846c31bb9..ce41b57c4ca4 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayListRepository.kt @@ -11,11 +11,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.GeoLocationId import net.mullvad.mullvadvpn.lib.model.PortRange import net.mullvad.mullvadvpn.lib.model.RelayItem import net.mullvad.mullvadvpn.lib.model.RelayItemId import net.mullvad.mullvadvpn.lib.model.WireguardConstraints import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData +import net.mullvad.mullvadvpn.relaylist.findByGeoLocationId class RelayListRepository( private val managementService: ManagementService, @@ -49,5 +51,7 @@ class RelayListRepository( suspend fun updateSelectedWireguardConstraints(value: WireguardConstraints) = managementService.setWireguardConstraints(value) + fun find(geoLocationId: GeoLocationId) = relayList.value.findByGeoLocationId(geoLocationId) + private fun defaultWireguardEndpointData() = WireguardEndpointData(emptyList()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt index e367386bf265..bbe51c0a8f24 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/CreateCustomListDialogViewModel.kt @@ -12,8 +12,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import net.mullvad.mullvadvpn.compose.communication.Created import net.mullvad.mullvadvpn.compose.communication.CustomListAction +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName @@ -58,7 +58,13 @@ class CreateCustomListDialogViewModel( ) } else { _uiSideEffect.send( - CreateCustomListDialogSideEffect.ReturnWithResult(it) + CreateCustomListDialogSideEffect.ReturnWithResult( + CustomListActionResultData.CreatedWithLocations( + customListName = it.name, + locationNames = it.locationNames, + undo = it.undo + ) + ) ) } } @@ -76,5 +82,6 @@ sealed interface CreateCustomListDialogSideEffect { data class NavigateToCustomListLocationsScreen(val customListId: CustomListId) : CreateCustomListDialogSideEffect - data class ReturnWithResult(val result: Created) : CreateCustomListDialogSideEffect + data class ReturnWithResult(val result: CustomListActionResultData.CreatedWithLocations) : + CreateCustomListDialogSideEffect } 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 7fa101a8e370..ac89821846cf 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 @@ -16,7 +16,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.LocationsChanged +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState import net.mullvad.mullvadvpn.compose.state.RelayLocationListItem import net.mullvad.mullvadvpn.lib.model.RelayItem @@ -108,23 +108,55 @@ class CustomListLocationsViewModel( fun save() { viewModelScope.launch { _selectedLocations.value?.let { selectedLocations -> + val locationsToSave = selectedLocations.calculateLocationsToSave() customListActionUseCase( CustomListAction.UpdateLocations( navArgs.customListId, - selectedLocations.calculateLocationsToSave().map { it.id } + locationsToSave.map { it.id } ) ) .fold( { _uiSideEffect.tryEmit(CustomListLocationsSideEffect.Error) }, - { - _uiSideEffect.tryEmit( - // This is so that we don't show a snackbar after returning to the - // select location screen + { result -> + val resultData = if (navArgs.newList) { - CustomListLocationsSideEffect.CloseScreen + CustomListActionResultData.CreatedWithLocations( + customListName = result.name, + locationNames = locationsToSave.map { it.name }, + undo = CustomListAction.Delete(id = result.id) + ) } else { - CustomListLocationsSideEffect.ReturnWithResult(it) + when { + result.addedLocations.size == 1 && + result.removedLocations.isEmpty() -> + CustomListActionResultData.LocationAdded( + customListName = result.name, + relayListRepository + .find(result.removedLocations.first())!! + .name, + undo = result.undo + ) + result.removedLocations.size == 1 && + result.addedLocations.isEmpty() -> + CustomListActionResultData.LocationRemoved( + customListName = result.name, + locationName = + relayListRepository + .find(result.removedLocations.first())!! + .name, + undo = result.undo + ) + else -> + CustomListActionResultData.LocationChanged( + customListName = result.name, + undo = result.undo + ) + } } + _uiSideEffect.tryEmit( + CustomListLocationsSideEffect.ReturnWithResultData( + result = resultData + ) ) } ) @@ -283,9 +315,8 @@ class CustomListLocationsViewModel( } sealed interface CustomListLocationsSideEffect { - data object CloseScreen : CustomListLocationsSideEffect - - data class ReturnWithResult(val result: LocationsChanged) : CustomListLocationsSideEffect + data class ReturnWithResultData(val result: CustomListActionResultData) : + CustomListLocationsSideEffect data object Error : CustomListLocationsSideEffect } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt index c492bd368a30..6a163517a0fe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeleteCustomListConfirmationViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.communication.Deleted import net.mullvad.mullvadvpn.compose.state.DeleteCustomListUiState import net.mullvad.mullvadvpn.lib.model.CustomListId @@ -49,7 +50,12 @@ class DeleteCustomListConfirmationViewModel( { _error.tryEmit(it) }, { _uiSideEffect.send( - DeleteCustomListConfirmationSideEffect.ReturnWithResult(it) + DeleteCustomListConfirmationSideEffect.ReturnWithResult( + CustomListActionResultData.Deleted( + customListName = it.name, + undo = it.undo + ) + ) ) } ) @@ -58,5 +64,6 @@ class DeleteCustomListConfirmationViewModel( } sealed interface DeleteCustomListConfirmationSideEffect { - data class ReturnWithResult(val result: Deleted) : DeleteCustomListConfirmationSideEffect + data class ReturnWithResult(val result: CustomListActionResultData.Deleted) : + DeleteCustomListConfirmationSideEffect } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt index 0b71a5053ead..934cdb7471c6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/EditCustomListNameDialogViewModel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.Renamed +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.state.EditCustomListNameUiState import net.mullvad.mullvadvpn.lib.model.CustomListName import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase @@ -52,7 +52,16 @@ class EditCustomListNameDialogViewModel( ) .fold( { _error.emit(it) }, - { _uiSideEffect.send(EditCustomListNameDialogSideEffect.ReturnWithResult(it)) } + { + _uiSideEffect.send( + EditCustomListNameDialogSideEffect.ReturnWithResult( + CustomListActionResultData.Renamed( + newName = it.name, + undo = it.undo + ) + ) + ) + } ) } } @@ -64,5 +73,6 @@ class EditCustomListNameDialogViewModel( } sealed interface EditCustomListNameDialogSideEffect { - data class ReturnWithResult(val result: Renamed) : EditCustomListNameDialogSideEffect + data class ReturnWithResult(val result: CustomListActionResultData.Renamed) : + EditCustomListNameDialogSideEffect } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt index f31fcb307840..da68fff305fe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModel.kt @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse import arrow.core.raise.either import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -14,7 +15,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.LocationsChanged +import net.mullvad.mullvadvpn.compose.communication.CustomListActionResultData import net.mullvad.mullvadvpn.compose.state.FilterChip import net.mullvad.mullvadvpn.compose.state.RelayListItem import net.mullvad.mullvadvpn.compose.state.RelayListItem.CustomListHeader @@ -44,7 +45,7 @@ import net.mullvad.mullvadvpn.usecase.customlists.FilterCustomListsRelayItemUseC class SelectLocationViewModel( private val relayListFilterRepository: RelayListFilterRepository, private val availableProvidersUseCase: AvailableProvidersUseCase, - private val customListsRelayItemUseCase: CustomListsRelayItemUseCase, + customListsRelayItemUseCase: CustomListsRelayItemUseCase, private val filteredCustomListRelayItemsUseCase: FilterCustomListsRelayItemUseCase, private val customListsRepository: CustomListsRepository, private val customListActionUseCase: CustomListActionUseCase, @@ -79,8 +80,7 @@ class SelectLocationViewModel( val uiSideEffect = _uiSideEffect.receiveAsFlow() private fun initialExpand(): Set = buildSet { - val item = relayListRepository.selectedLocation.value.getOrNull() - when (item) { + when (val item = relayListRepository.selectedLocation.value.getOrNull()) { is GeoLocationId.City -> { add(item.country.code) } @@ -131,7 +131,7 @@ class SelectLocationViewModel( .size } - buildList { + buildList { if (ownershipFilter != null) { add(FilterChip.Ownership(ownershipFilter)) } @@ -157,9 +157,10 @@ class SelectLocationViewModel( searchTerm.length >= MIN_SEARCH_LENGTH, selectedItem.getOrNull(), filteredCustomLists, - relayCountries, - { it in expandedItems } - ) + relayCountries + ) { + it in expandedItems + } if (relayItems.isEmpty()) { add(RelayListItem.LocationsEmptyText(searchTerm)) } else { @@ -201,7 +202,7 @@ class SelectLocationViewModel( ): List = customLists.flatMap { customList -> val expanded = isExpanded(customList.id.expandKey()) - buildList { + buildList { add( RelayListItem.CustomListItem( customList, @@ -243,36 +244,35 @@ class SelectLocationViewModel( item: RelayItem.Location, depth: Int = 1, isExpanded: (String) -> Boolean, - ): List = - buildList { - val expanded = isExpanded(item.id.expandKey(parent)) - add( - RelayListItem.CustomListEntryItem( - parentId = parent, - item = item, - expanded = expanded, - depth - ) + ): List = buildList { + val expanded = isExpanded(item.id.expandKey(parent)) + add( + RelayListItem.CustomListEntryItem( + parentId = parent, + item = item, + expanded = expanded, + depth ) + ) - if (expanded) { - when (item) { - is RelayItem.Location.City -> - addAll( - item.relays.flatMap { - createCustomListEntry(parent, it, depth + 1, isExpanded) - } - ) - is RelayItem.Location.Country -> - addAll( - item.cities.flatMap { - createCustomListEntry(parent, it, depth + 1, isExpanded) - } - ) - is RelayItem.Location.Relay -> {} // No children to add - } + if (expanded) { + when (item) { + is RelayItem.Location.City -> + addAll( + item.relays.flatMap { + createCustomListEntry(parent, it, depth + 1, isExpanded) + } + ) + is RelayItem.Location.Country -> + addAll( + item.cities.flatMap { + createCustomListEntry(parent, it, depth + 1, isExpanded) + } + ) + is RelayItem.Location.Relay -> {} // No children to add } } + } private fun createGeoLocationEntry( item: RelayItem.Location, @@ -363,11 +363,28 @@ class SelectLocationViewModel( viewModelScope.launch { val newLocations = (customList.locations + item).filter { it !in item.descendants() }.map { it.id } - customListActionUseCase(CustomListAction.UpdateLocations(customList.id, newLocations)) - .fold( - { _uiSideEffect.send(SelectLocationSideEffect.GenericError) }, - { _uiSideEffect.send(SelectLocationSideEffect.LocationAddedToCustomList(it)) }, - ) + val result = + customListActionUseCase( + CustomListAction.UpdateLocations(customList.id, newLocations) + ) + .fold( + { CustomListActionResultData.GenericError }, + { + if (it.removedLocations.isEmpty()) { + CustomListActionResultData.LocationAdded( + customListName = it.name, + locationName = item.name, + undo = it.undo + ) + } else { + CustomListActionResultData.LocationChanged( + customListName = it.name, + undo = it.undo + ) + } + }, + ) + _uiSideEffect.send(SelectLocationSideEffect.CustomListActionToast(result)) } } @@ -382,17 +399,26 @@ class SelectLocationViewModel( val customList = customListsRepository.getCustomListById(customListId).bind() val newLocations = (customList.locations - item.id) - - customListActionUseCase( - CustomListAction.UpdateLocations(customList.id, newLocations) + val success = + customListActionUseCase( + CustomListAction.UpdateLocations(customList.id, newLocations) + ) + .bind() + if (success.addedLocations.isEmpty()) { + CustomListActionResultData.LocationRemoved( + customListName = success.name, + locationName = item.name, + undo = success.undo + ) + } else { + CustomListActionResultData.LocationChanged( + customListName = success.name, + undo = success.undo ) - .bind() + } } - .fold( - { SelectLocationSideEffect.GenericError }, - { SelectLocationSideEffect.LocationRemovedFromCustomList(it) } - ) - _uiSideEffect.send(result) + .getOrElse { CustomListActionResultData.GenericError } + _uiSideEffect.send(SelectLocationSideEffect.CustomListActionToast(result)) } } @@ -404,9 +430,12 @@ class SelectLocationViewModel( sealed interface SelectLocationSideEffect { data object CloseScreen : SelectLocationSideEffect - data class LocationAddedToCustomList(val result: LocationsChanged) : SelectLocationSideEffect + // data class LocationAddedToCustomList(val result: LocationsChanged) : SelectLocationSideEffect + + // class LocationRemovedFromCustomList(val result: LocationsChanged) : SelectLocationSideEffect - class LocationRemovedFromCustomList(val result: LocationsChanged) : SelectLocationSideEffect + data class CustomListActionToast(val resultData: CustomListActionResultData) : + SelectLocationSideEffect data object GenericError : SelectLocationSideEffect } diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 099d14fda973..531a9b046b94 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -384,4 +384,5 @@ Failed to set to current - API not reachable Failed to set to current - Unknown reason %s was removed from \"%s\" + \"%s\" was created