diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt index bfe95234cec2..d9bf076e547f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListAction.kt @@ -3,34 +3,33 @@ package net.mullvad.mullvadvpn.compose.communication import android.os.Parcelable import kotlinx.parcelize.Parcelize -sealed interface CustomListAction : Parcelable { +sealed interface +CustomListAction : Parcelable { @Parcelize - data class Rename(val customListId: String, val name: String) : CustomListAction { - fun not(): CustomListAction = this + data class Rename(val customListId: String, val name: String, val newName: String) : CustomListAction { + fun not() = this.copy(name = newName, newName = name) } @Parcelize - data class Delete(val customListId: String, val name: String) : CustomListAction { - fun not(locations: List): CustomListAction = Create(name, locations) + data class Delete(val customListId: String) : CustomListAction { + fun not(name: String, locations: List) = Create(name, locations) } @Parcelize data class Create( val name: String = "", - val locations: List = emptyList(), - val locationNames: List = emptyList() + val locations: List = emptyList() ) : CustomListAction, Parcelable { - fun not(customListId: String) = Delete(customListId, name) + fun not(customListId: String) = Delete(customListId) } @Parcelize data class UpdateLocations( val customListId: String, - val newList: Boolean = false, val locations: List = emptyList() ) : CustomListAction { - fun not(locations: List): CustomListAction = + fun not(locations: List): UpdateLocations = UpdateLocations(customListId = customListId, locations = locations) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListRequest.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListRequest.kt deleted file mode 100644 index eb303bed2be8..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListRequest.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.mullvadvpn.compose.communication - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize @JvmInline value class CustomListRequest(val action: CustomListAction) : Parcelable - -inline fun CustomListRequest.parsedAction(): T = action as T diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt index 3067ec12249c..32fa077a7f18 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/communication/CustomListResult.kt @@ -4,24 +4,31 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize sealed interface CustomListResult : Parcelable { - val reverseAction: CustomListAction + val undo: CustomListAction @Parcelize - data class ListCreated( - val locationName: String, - val customListName: String, - override val reverseAction: CustomListAction + data class Created( + val id: String, + val name: String, + val locationName: String?, + override val undo: CustomListAction.Delete ) : CustomListResult @Parcelize - data class ListDeleted(val name: String, override val reverseAction: CustomListAction) : - CustomListResult + data class Deleted(override val undo: CustomListAction.Create) : CustomListResult { + val name + get() = undo.name + } @Parcelize - data class ListRenamed(val name: String, override val reverseAction: CustomListAction) : - CustomListResult + data class Renamed(override val undo: CustomListAction.Rename) : CustomListResult { + val name: String + get() = undo.name + } @Parcelize - data class ListUpdated(val name: String, override val reverseAction: CustomListAction) : - CustomListResult + data class LocationsChanged( + val name: String, + override val undo: CustomListAction.UpdateLocations + ) : CustomListResult } 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 59bf089bd613..be3f2e763a3c 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,10 +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.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult -import net.mullvad.mullvadvpn.compose.communication.parsedAction import net.mullvad.mullvadvpn.compose.destinations.CustomListLocationsDestination import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState import net.mullvad.mullvadvpn.compose.textfield.CustomTextField @@ -57,28 +54,21 @@ fun PreviewCreateCustomListDialogError() { @Destination(style = DestinationStyle.Dialog::class) fun CreateCustomList( navigator: DestinationsNavigator, - backNavigator: ResultBackNavigator, - request: CustomListRequest + backNavigator: ResultBackNavigator, + locationCode: String = "" ) { val vm: CreateCustomListDialogViewModel = koinViewModel( - parameters = { parametersOf(request.parsedAction()) } + parameters = { parametersOf(locationCode) } ) LaunchedEffect(key1 = Unit) { vm.uiSideEffect.collect { sideEffect -> when (sideEffect) { is CreateCustomListDialogSideEffect.NavigateToCustomListLocationsScreen -> { - navigator.popBackStack() navigator.navigate( CustomListLocationsDestination( - request = - CustomListRequest( - action = - CustomListAction.UpdateLocations( - customListId = sideEffect.customListId, - newList = true - ) - ) + customListId = sideEffect.customListId, + newList = true ) ) { launchSingleTop = true 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 9e9cbe241a74..c03c2d275112 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 @@ -21,10 +21,7 @@ import com.ramcosta.composedestinations.spec.DestinationStyle import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.NegativeButton import net.mullvad.mullvadvpn.compose.button.PrimaryButton -import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult -import net.mullvad.mullvadvpn.compose.communication.parsedAction import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.viewmodel.DeleteCustomListConfirmationSideEffect @@ -41,11 +38,12 @@ private fun PreviewRemoveDeviceConfirmationDialog() { @Composable @Destination(style = DestinationStyle.Dialog::class) fun DeleteCustomList( - navigator: ResultBackNavigator, - request: CustomListRequest + navigator: ResultBackNavigator, + customListId: String, + name: String ) { val viewModel: DeleteCustomListConfirmationViewModel = - koinViewModel(parameters = { parametersOf(request.parsedAction()) }) + koinViewModel(parameters = { parametersOf(customListId) }) LaunchedEffect(Unit) { viewModel.uiSideEffect.collect { @@ -57,7 +55,7 @@ fun DeleteCustomList( } DeleteCustomListConfirmationDialog( - name = request.parsedAction().name, + name = name, onDelete = viewModel::deleteCustomList, onBack = { navigator.navigateBack() } ) 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 3ae25d5d3939..0bd439c9cf52 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 @@ -22,9 +22,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.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult -import net.mullvad.mullvadvpn.compose.communication.parsedAction import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState import net.mullvad.mullvadvpn.compose.textfield.CustomTextField import net.mullvad.mullvadvpn.lib.theme.AppTheme @@ -43,11 +41,12 @@ fun PreviewEditCustomListNameDialog() { @Composable @Destination(style = DestinationStyle.Dialog::class) fun EditCustomListName( - backNavigator: ResultBackNavigator, - request: CustomListRequest + backNavigator: ResultBackNavigator, + customListId: String, + initialName: String ) { val vm: EditCustomListNameDialogViewModel = - koinViewModel(parameters = { parametersOf(request.parsedAction()) }) + koinViewModel(parameters = { parametersOf(customListId, initialName) }) LaunchedEffect(Unit) { vm.uiSideEffect.collect { sideEffect -> when (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 b565751d7cf1..9e4b64370a80 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 @@ -28,9 +28,7 @@ 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.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult -import net.mullvad.mullvadvpn.compose.communication.parsedAction import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton @@ -59,13 +57,14 @@ fun PreviewCustomListLocationScreen() { @Destination(style = SlideInFromRightTransition::class) fun CustomListLocations( navigator: DestinationsNavigator, - backNavigator: ResultBackNavigator, - request: CustomListRequest, - discardChangesResultRecipient: ResultRecipient + backNavigator: ResultBackNavigator, + discardChangesResultRecipient: ResultRecipient, + customListId: String, + newList: Boolean, ) { val customListsViewModel = koinViewModel( - parameters = { parametersOf(request.parsedAction()) } + parameters = { parametersOf(customListId, newList) } ) discardChangesResultRecipient.onNavResult( 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 e0d3b6cb9e2e..e16e412c129f 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 @@ -21,9 +21,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult @@ -32,7 +35,6 @@ import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.NavigationComposeCell import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton @@ -62,7 +64,7 @@ fun PreviewCustomListsScreen() { fun CustomLists( navigator: DestinationsNavigator, editCustomListResultRecipient: - ResultRecipient + ResultRecipient ) { val viewModel = koinViewModel() val uiState by viewModel.uiState.collectAsState() @@ -89,7 +91,7 @@ fun CustomLists( duration = SnackbarDuration.Long, onAction = { viewModel.undoDeleteCustomList( - result.value.reverseAction as CustomListAction.Create + result.value.undo as CustomListAction.Create ) } ) @@ -104,7 +106,7 @@ fun CustomLists( snackbarHostState = snackbarHostState, addCustomList = { navigator.navigate( - CreateCustomListDestination(CustomListRequest(CustomListAction.Create())), + CreateCustomListDestination(), ) { launchSingleTop = true } @@ -131,6 +133,17 @@ fun CustomListsScreen( navigationIcon = { NavigateBackIconButton(onBackClick) }, floatingActionButton = { ExtendedFloatingActionButton( + modifier = + Modifier.shadow( + elevation = 3.dp, + spotColor = Color(0x4D000000), + ambientColor = Color(0x4D000000) + ) + .shadow( + elevation = 8.dp, + spotColor = Color(0x26000000), + ambientColor = Color(0x26000000) + ), onClick = addCustomList, containerColor = MaterialTheme.colorScheme.primary, contentColor = MaterialTheme.colorScheme.onPrimary, 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 0edf571c88f2..d821af37d1ef 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 @@ -26,8 +26,6 @@ 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.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton @@ -76,10 +74,10 @@ fun PreviewEditCustomListScreen() { @Destination(style = SlideInFromRightTransition::class) fun EditCustomList( navigator: DestinationsNavigator, - backNavigator: ResultBackNavigator, + backNavigator: ResultBackNavigator, customListId: String, confirmDeleteListResultRecipient: - ResultRecipient + ResultRecipient ) { val viewModel = koinViewModel(parameters = { parametersOf(customListId) }) @@ -98,32 +96,20 @@ fun EditCustomList( uiState = uiState, onDeleteList = { name -> navigator.navigate( - DeleteCustomListDestination( - CustomListRequest(action = CustomListAction.Delete(customListId, name)) - ) + DeleteCustomListDestination(customListId = customListId, name = name) ) { launchSingleTop = true } }, onNameClicked = { id, name -> navigator.navigate( - EditCustomListNameDestination( - request = CustomListRequest(action = CustomListAction.Rename(id, name)) - ) + EditCustomListNameDestination(customListId = id, initialName = name) ) { launchSingleTop = true } }, onLocationsClicked = { - navigator.navigate( - CustomListLocationsDestination( - request = - CustomListRequest( - action = - CustomListAction.UpdateLocations(customListId = it, newList = false) - ) - ) - ) { + navigator.navigate(CustomListLocationsDestination(customListId = it, newList = false)) { launchSingleTop = true } }, 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 801a0931b8d3..67c823b95724 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 @@ -48,6 +48,7 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient +import com.ramcosta.composedestinations.spec.DestinationSpec import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.cell.FilterCell @@ -57,7 +58,6 @@ import net.mullvad.mullvadvpn.compose.cell.NormalRelayLocationCell import net.mullvad.mullvadvpn.compose.cell.SwitchComposeSubtitleCell import net.mullvad.mullvadvpn.compose.cell.ThreeDotCell import net.mullvad.mullvadvpn.compose.communication.CustomListAction -import net.mullvad.mullvadvpn.compose.communication.CustomListRequest import net.mullvad.mullvadvpn.compose.communication.CustomListResult import net.mullvad.mullvadvpn.compose.component.LocationsEmptyText import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge @@ -112,19 +112,18 @@ private fun PreviewSelectLocationScreen() { fun SelectLocation( navigator: DestinationsNavigator, createCustomListDialogResultRecipient: - ResultRecipient, + ResultRecipient, editCustomListNameDialogResultRecipient: - ResultRecipient, + ResultRecipient, deleteCustomListDialogResultRecipient: - ResultRecipient, + ResultRecipient, updateCustomListResultRecipient: - ResultRecipient + ResultRecipient ) { val vm = koinViewModel() val state = vm.uiState.collectAsState().value val snackbarHostState = remember { SnackbarHostState() } - val scope = rememberCoroutineScope() val context = LocalContext.current LaunchedEffect(Unit) { @@ -133,22 +132,10 @@ fun SelectLocation( SelectLocationSideEffect.CloseScreen -> navigator.navigateUp() is SelectLocationSideEffect.LocationAddedToCustomList -> { launch { - snackbarHostState.currentSnackbarData?.dismiss() - snackbarHostState.showSnackbar( - message = - context.getString( - R.string.location_was_added_to_list, - it.item.name, - it.customList.name - ), - actionLabel = context.getString(R.string.undo), - duration = SnackbarDuration.Long, - onAction = { - vm.removeLocationFromList( - item = it.item, - customList = it.customList - ) - } + snackbarHostState.showResultSnackbar( + context = context, + result = it.result, + onUndo = vm::performAction ) } } @@ -156,73 +143,22 @@ fun SelectLocation( } } - createCustomListDialogResultRecipient.onNavResult { result -> - when (result) { - NavResult.Canceled -> { - /* Do nothing */ - } - is NavResult.Value -> { - scope.launch { - snackbarHostState.showResultSnackbar( - context = context, - result = result.value, - onAction = { action -> vm.performAction(action) } - ) - } - } - } - } + createCustomListDialogResultRecipient.OnCustomListNavResult( + snackbarHostState, + vm::performAction + ) - editCustomListNameDialogResultRecipient.onNavResult { result -> - when (result) { - NavResult.Canceled -> { - /* Do nothing */ - } - is NavResult.Value -> { - scope.launch { - snackbarHostState.showResultSnackbar( - context = context, - result = result.value, - onAction = { action -> vm.performAction(action) } - ) - } - } - } - } + editCustomListNameDialogResultRecipient.OnCustomListNavResult( + snackbarHostState, + vm::performAction + ) - deleteCustomListDialogResultRecipient.onNavResult { result -> - when (result) { - NavResult.Canceled -> { - /* Do nothing */ - } - is NavResult.Value -> { - scope.launch { - snackbarHostState.showResultSnackbar( - context = context, - result = result.value, - onAction = { action -> vm.performAction(action) } - ) - } - } - } - } + deleteCustomListDialogResultRecipient.OnCustomListNavResult( + snackbarHostState, + vm::performAction + ) - updateCustomListResultRecipient.onNavResult { result -> - when (result) { - NavResult.Canceled -> { - /* Do nothing */ - } - is NavResult.Value -> { - scope.launch { - snackbarHostState.showResultSnackbar( - context = context, - result = result.value, - onAction = { action -> vm.performAction(action) } - ) - } - } - } - } + updateCustomListResultRecipient.OnCustomListNavResult(snackbarHostState, vm::performAction) SelectLocationScreen( uiState = state, @@ -232,17 +168,7 @@ fun SelectLocation( onBackClick = navigator::navigateUp, onFilterClick = { navigator.navigate(FilterScreenDestination) }, onCreateCustomList = { relayItem -> - navigator.navigate( - CreateCustomListDestination( - CustomListRequest( - action = - CustomListAction.Create( - locations = relayItem?.code?.let { listOf(it) } ?: emptyList(), - locationNames = relayItem?.name?.let { listOf(it) } ?: emptyList() - ) - ) - ) - ) { + navigator.navigate(CreateCustomListDestination(locationCode = relayItem?.code ?: "")) { launchSingleTop = true } }, @@ -252,28 +178,16 @@ fun SelectLocation( onAddLocationToList = vm::addLocationToList, onEditCustomListName = { navigator.navigate( - EditCustomListNameDestination( - CustomListRequest( - action = CustomListAction.Rename(customListId = it.id, name = it.name) - ) - ) + EditCustomListNameDestination(customListId = it.id, initialName = it.name) ) }, onEditLocationsCustomList = { navigator.navigate( - CustomListLocationsDestination( - CustomListRequest(action = CustomListAction.UpdateLocations(it.id, false)) - ) + CustomListLocationsDestination(customListId = it.id, newList = false) ) }, onDeleteCustomList = { - navigator.navigate( - DeleteCustomListDestination( - CustomListRequest( - action = CustomListAction.Delete(customListId = it.id, name = it.name) - ) - ) - ) + navigator.navigate(DeleteCustomListDestination(customListId = it.id, name = it.name)) } ) } @@ -744,28 +658,54 @@ private suspend fun LazyListState.animateScrollAndCentralizeItem(index: Int) { private suspend fun SnackbarHostState.showResultSnackbar( context: Context, result: CustomListResult, - onAction: (CustomListAction) -> Unit + onUndo: (CustomListAction) -> Unit ) { currentSnackbarData?.dismiss() showSnackbar( message = result.message(context), actionLabel = context.getString(R.string.undo), duration = SnackbarDuration.Long, - onAction = { onAction(result.reverseAction) } + onAction = { onUndo(result.undo) } ) } private fun CustomListResult.message(context: Context): String = when (this) { - is CustomListResult.ListCreated -> - context.getString(R.string.location_was_added_to_list, locationName, customListName) - is CustomListResult.ListDeleted -> - context.getString(R.string.delete_custom_list_message, name) - is CustomListResult.ListRenamed -> context.getString(R.string.name_was_changed_to, name) - is CustomListResult.ListUpdated -> + is CustomListResult.Created -> + context.getString(R.string.location_was_added_to_list, locationName, name) + is CustomListResult.Deleted -> context.getString(R.string.delete_custom_list_message, name) + is CustomListResult.Renamed -> context.getString(R.string.name_was_changed_to, name) + is CustomListResult.LocationsChanged -> context.getString(R.string.locations_were_changed_for, name) } +@Composable +private fun , R : CustomListResult> ResultRecipient + .OnCustomListNavResult( + snackbarHostState: SnackbarHostState, + performAction: (action: CustomListAction) -> Unit +) { + val scope = rememberCoroutineScope() + val context = LocalContext.current + this.onNavResult { result -> + when (result) { + NavResult.Canceled -> { + /* Do nothing */ + } + is NavResult.Value -> { + // Handle result + scope.launch { + snackbarHostState.showResultSnackbar( + context = context, + result = result.value, + onUndo = performAction + ) + } + } + } + } +} + private const val EXTRA_ITEMS_LOCATION = 3 private const val EXTRA_ITEM_CUSTOM_LIST = 1 diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index a51f1e6b06e3..e7a1da4ee57a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -37,6 +37,7 @@ import net.mullvad.mullvadvpn.usecase.RelayListUseCase import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase import net.mullvad.mullvadvpn.usecase.VersionNotificationUseCase +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase import net.mullvad.mullvadvpn.util.ChangelogDataProvider import net.mullvad.mullvadvpn.util.IChangelogDataProvider import net.mullvad.mullvadvpn.viewmodel.AccountViewModel @@ -117,6 +118,7 @@ val uiModule = module { single { OutOfTimeUseCase(get(), get()) } single { ConnectivityUseCase(get()) } single { SystemVpnSettingsUseCase(androidContext()) } + single { CustomListActionUseCase(get(), get()) } single { InAppNotificationController(get(), get(), get(), get(), MainScope()) } @@ -167,9 +169,13 @@ val uiModule = module { viewModel { PaymentViewModel(get()) } viewModel { FilterViewModel(get()) } viewModel { parameters -> CreateCustomListDialogViewModel(parameters.get(), get()) } - viewModel { parameters -> CustomListLocationsViewModel(parameters.get(), get(), get()) } + viewModel { parameters -> + CustomListLocationsViewModel(parameters.get(), parameters.get(), get(), get()) + } viewModel { parameters -> EditCustomListViewModel(parameters.get(), get()) } - viewModel { parameters -> EditCustomListNameDialogViewModel(parameters.get(), get()) } + viewModel { parameters -> + EditCustomListNameDialogViewModel(parameters.get(), parameters.get(), get()) + } viewModel { CustomListsViewModel(get(), get()) } viewModel { parameters -> DeleteCustomListConfirmationViewModel(parameters.get(), get()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt index 0a3fb09afbd1..2fcab76572b1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/CustomListExtensions.kt @@ -29,3 +29,5 @@ fun List.filterOnSearchTerm(searchTerm: String) = fun RelayItem.CustomList.canAddLocation(location: RelayItem) = this.locations.none { it.code == location.code } && this.locations.flatMap { it.allChildren() }.none { it.code == location.code } + +fun List.getById(id: String) = this.find { it.id == id } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt index 127c671f45e9..7e0be663592e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayListExtensions.kt @@ -260,3 +260,7 @@ fun RelayList.getGeographicLocationConstraintByCode(code: String): GeographicLoc } return null } + +fun List.getRelayItemsByCodes(codes: List): List = + this.filter { codes.contains(it.code) } + + this.flatMap { it.allChildren() }.filter { codes.contains(it.code) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt index a77d49e80968..46909fb53af1 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/CustomListsRepository.kt @@ -46,16 +46,6 @@ class CustomListsRepository( } } - suspend fun updateCustomListLocations( - id: String, - locations: List - ): UpdateCustomListResult { - return updateCustomListLocations( - id = id, - locations = locations.toGeographicLocationConstraints() - ) - } - suspend fun updateCustomListLocationsFromCodes( id: String, locationCodes: List diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt new file mode 100644 index 000000000000..7b2e5a43aa00 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListActionUseCase.kt @@ -0,0 +1,117 @@ +package net.mullvad.mullvadvpn.usecase.customlists + +import kotlinx.coroutines.flow.firstOrNull +import net.mullvad.mullvadvpn.compose.communication.CustomListAction +import net.mullvad.mullvadvpn.compose.communication.CustomListResult +import net.mullvad.mullvadvpn.model.CreateCustomListResult +import net.mullvad.mullvadvpn.model.CustomList +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint +import net.mullvad.mullvadvpn.model.UpdateCustomListResult +import net.mullvad.mullvadvpn.relaylist.getRelayItemsByCodes +import net.mullvad.mullvadvpn.repository.CustomListsRepository +import net.mullvad.mullvadvpn.usecase.RelayListUseCase + +class CustomListActionUseCase( + private val customListsRepository: CustomListsRepository, + private val relayListUseCase: RelayListUseCase +) { + suspend fun performAction(action: CustomListAction): Result { + return when (action) { + is CustomListAction.Create -> { + performAction(action) + } + is CustomListAction.Rename -> { + performAction(action) + } + is CustomListAction.Delete -> { + performAction(action) + } + is CustomListAction.UpdateLocations -> { + performAction(action) + } + } + } + + suspend fun performAction(action: CustomListAction.Rename): Result = + when ( + val result = + customListsRepository.updateCustomListName(action.customListId, action.newName) + ) { + is UpdateCustomListResult.Ok -> + Result.success(CustomListResult.Renamed(undo = action.not())) + is UpdateCustomListResult.Error -> Result.failure(CustomListsException(result.error)) + } + + suspend fun performAction(action: CustomListAction.Create): Result = + when (val result = customListsRepository.createCustomList(action.name)) { + is CreateCustomListResult.Ok -> { + if (action.locations.isNotEmpty()) { + customListsRepository.updateCustomListLocationsFromCodes( + result.id, + action.locations + ) + val locationNames = + relayListUseCase + .relayList() + .firstOrNull() + ?.getRelayItemsByCodes(action.locations) + ?.map { it.name } + Result.success( + CustomListResult.Created( + id = result.id, + name = action.name, + locationName = locationNames?.first(), + undo = action.not(result.id) + ) + ) + } else { + Result.success( + CustomListResult.Created( + id = result.id, + name = action.name, + locationName = null, + undo = action.not(result.id) + ) + ) + } + } + is CreateCustomListResult.Error -> Result.failure(CustomListsException(result.error)) + } + + fun performAction(action: CustomListAction.Delete): Result { + val customList: CustomList? = customListsRepository.getCustomListById(action.customListId) + val oldLocations = customList.locations() + val name = customList?.name ?: "" + customListsRepository.deleteCustomList(action.customListId) + return Result.success( + CustomListResult.Deleted(undo = action.not(locations = oldLocations, name = name)) + ) + } + + suspend fun performAction( + action: CustomListAction.UpdateLocations + ): Result { + val customList: CustomList? = customListsRepository.getCustomListById(action.customListId) + val oldLocations = customList.locations() + val name = customList?.name ?: "" + customListsRepository.updateCustomListLocationsFromCodes( + action.customListId, + action.locations + ) + return Result.success( + CustomListResult.LocationsChanged( + name = name, + undo = action.not(locations = oldLocations) + ) + ) + } + + private fun CustomList?.locations(): List = + this?.locations?.map { + when (it) { + is GeographicLocationConstraint.City -> it.cityCode + is GeographicLocationConstraint.Country -> it.countryCode + is GeographicLocationConstraint.Hostname -> it.hostname + } + } ?: emptyList() +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt new file mode 100644 index 000000000000..07c37f733332 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/CustomListsException.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.usecase.customlists + +import net.mullvad.mullvadvpn.model.CustomListsError + +class CustomListsException(val error: CustomListsError) : Throwable() 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 a02d5e53a701..9ae5bb7a648b 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 @@ -13,14 +13,13 @@ import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.CustomListResult import net.mullvad.mullvadvpn.compose.state.CreateCustomListUiState -import net.mullvad.mullvadvpn.model.CreateCustomListResult import net.mullvad.mullvadvpn.model.CustomListsError -import net.mullvad.mullvadvpn.model.UpdateCustomListResult -import net.mullvad.mullvadvpn.repository.CustomListsRepository +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase +import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException class CreateCustomListDialogViewModel( - private val action: CustomListAction.Create, - private val customListsRepository: CustomListsRepository, + private val locationCode: String, + private val customListActionUseCase: CustomListActionUseCase, ) : ViewModel() { private val _uiSideEffect = @@ -36,59 +35,38 @@ class CreateCustomListDialogViewModel( fun createCustomList(name: String) { viewModelScope.launch { - when (val result = customListsRepository.createCustomList(name)) { - is CreateCustomListResult.Ok -> { - // We want to create the custom list with a location - if (action.locations.isNotEmpty()) { - addCustomListToLocation( - result.id, - name, - action.locations.first(), - action.locationNames.first() - ) - } else { - // We want to create the custom list without a location - _uiSideEffect.send( - CreateCustomListDialogSideEffect.NavigateToCustomListLocationsScreen( - result.id + customListActionUseCase + .performAction( + CustomListAction.Create( + name, + if (locationCode.isNotEmpty()) { + listOf(locationCode) + } else { + emptyList() + } + ) + ) + .fold( + onSuccess = { result -> + if (result.locationName != null) { + _uiSideEffect.send( + CreateCustomListDialogSideEffect.ReturnWithResult(result) ) - ) + } else { + _uiSideEffect.send( + CreateCustomListDialogSideEffect + .NavigateToCustomListLocationsScreen(result.id) + ) + } + }, + onFailure = { error -> + if (error is CustomListsException) { + _error.emit(error.error) + } else { + _error.emit(CustomListsError.OtherError) + } } - } - is CreateCustomListResult.Error -> { - _error.emit(result.error) - } - } - } - } - - private suspend fun addCustomListToLocation( - customListId: String, - name: String, - locationCode: String, - locationName: String - ) { - when ( - val result = - customListsRepository.updateCustomListLocationsFromCodes( - customListId, - listOf(locationCode) - ) - ) { - is UpdateCustomListResult.Ok -> { - _uiSideEffect.send( - CreateCustomListDialogSideEffect.ReturnWithResult( - CustomListResult.ListCreated( - locationName = locationName, - customListName = name, - reverseAction = action.not(customListId) - ) - ) ) - } - is UpdateCustomListResult.Error -> { - _error.emit(result.error) - } } } @@ -102,6 +80,6 @@ sealed interface CreateCustomListDialogSideEffect { data class NavigateToCustomListLocationsScreen(val customListId: String) : CreateCustomListDialogSideEffect - data class ReturnWithResult(val result: CustomListResult.ListCreated) : + data class ReturnWithResult(val result: CustomListResult.Created) : 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 2ef4866b157a..e4190f48e1da 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 @@ -7,23 +7,27 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull 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.CustomListResult import net.mullvad.mullvadvpn.compose.state.CustomListLocationsUiState +import net.mullvad.mullvadvpn.model.CustomList import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.relaylist.allChildren import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm -import net.mullvad.mullvadvpn.repository.CustomListsRepository +import net.mullvad.mullvadvpn.relaylist.getById import net.mullvad.mullvadvpn.usecase.RelayListUseCase +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase +import net.mullvad.mullvadvpn.util.firstOrNullWithTimeout class CustomListLocationsViewModel( - private val action: CustomListAction.UpdateLocations, - relayListUseCase: RelayListUseCase, - private val customListsRepository: CustomListsRepository + private val customListId: String, + private val newList: Boolean, + private val relayListUseCase: RelayListUseCase, + private val customListActionUseCase: CustomListActionUseCase ) : ViewModel() { private var customListName: String = "" @@ -44,15 +48,15 @@ class CustomListLocationsViewModel( when { selectedLocations == null -> - CustomListLocationsUiState.Loading(newList = action.newList) + CustomListLocationsUiState.Loading(newList = newList) filteredRelayCountries.isEmpty() -> CustomListLocationsUiState.Content.Empty( - newList = action.newList, + newList = newList, searchTerm = searchTerm ) else -> CustomListLocationsUiState.Content.Data( - newList = action.newList, + newList = newList, searchTerm = searchTerm, availableLocations = filteredRelayCountries, selectedLocations = selectedLocations, @@ -66,16 +70,13 @@ class CustomListLocationsViewModel( .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - CustomListLocationsUiState.Loading(newList = action.newList) + CustomListLocationsUiState.Loading(newList = newList) ) init { viewModelScope.launch { _selectedLocations.value = - relayListUseCase - .customLists() - .firstOrNull() - ?.firstOrNull { it.id == action.customListId } + awaitCustomListById(customListId) ?.apply { customListName = name } ?.locations ?.selectChildren() @@ -86,22 +87,15 @@ class CustomListLocationsViewModel( fun save() { viewModelScope.launch { _selectedLocations.value?.let { selectedLocations -> - customListsRepository.updateCustomListLocations( - id = action.customListId, - locations = selectedLocations.calculateLocationsToSave() - ) - _uiSideEffect.tryEmit( - CustomListLocationsSideEffect.ReturnWithResult( - CustomListResult.ListUpdated( - name = customListName, - reverseAction = - action.not( - _initialLocations.value.calculateLocationsToSave().map { - it.code - } - ) + val result = + customListActionUseCase.performAction( + CustomListAction.UpdateLocations( + customListId, + selectedLocations.calculateLocationsToSave().map { it.code } ) ) + _uiSideEffect.tryEmit( + CustomListLocationsSideEffect.ReturnWithResult(result.getOrThrow()) ) } } @@ -119,6 +113,12 @@ class CustomListLocationsViewModel( viewModelScope.launch { _searchTerm.emit(searchTerm) } } + private suspend fun awaitCustomListById(id: String): RelayItem.CustomList? = + relayListUseCase + .customLists() + .mapNotNull { customList -> customList.getById(id) } + .firstOrNullWithTimeout(GET_CUSTOM_LIST_TIMEOUT_MS) + private fun selectLocation(relayItem: RelayItem) { viewModelScope.launch { _selectedLocations.update { @@ -197,10 +197,11 @@ class CustomListLocationsViewModel( companion object { private const val EMPTY_SEARCH_TERM = "" + private const val GET_CUSTOM_LIST_TIMEOUT_MS = 5000L } } sealed interface CustomListLocationsSideEffect { - data class ReturnWithResult(val result: CustomListResult.ListUpdated) : + data class ReturnWithResult(val result: CustomListResult.LocationsChanged) : 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 9f9f83a8fafb..e3c7f45664ec 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 @@ -7,41 +7,27 @@ import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.CustomListResult -import net.mullvad.mullvadvpn.model.GeographicLocationConstraint -import net.mullvad.mullvadvpn.repository.CustomListsRepository +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase class DeleteCustomListConfirmationViewModel( - private val action: CustomListAction.Delete, - private val customListsRepository: CustomListsRepository + private val customListId: String, + private val customListActionUseCase: CustomListActionUseCase ) : ViewModel() { private val _uiSideEffect = Channel(Channel.BUFFERED) val uiSideEffect = _uiSideEffect.receiveAsFlow() fun deleteCustomList() { viewModelScope.launch { - val oldLocations = - customListsRepository.getCustomListById(action.customListId)?.locations?.map { - when (it) { - is GeographicLocationConstraint.City -> it.cityCode - is GeographicLocationConstraint.Country -> it.countryCode - is GeographicLocationConstraint.Hostname -> it.hostname - } - } ?: emptyList() - customListsRepository.deleteCustomList(id = action.customListId) - _uiSideEffect.send( - DeleteCustomListConfirmationSideEffect.ReturnWithResult( - result = - CustomListResult.ListDeleted( - name = action.name, - reverseAction = action.not(oldLocations) - ) - ) - ) + val result = + customListActionUseCase + .performAction(CustomListAction.Delete(customListId)) + .getOrThrow() + _uiSideEffect.send(DeleteCustomListConfirmationSideEffect.ReturnWithResult(result)) } } } sealed class DeleteCustomListConfirmationSideEffect { - data class ReturnWithResult(val result: CustomListResult.ListDeleted) : + data class ReturnWithResult(val result: CustomListResult.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 b4e190a8e5ac..bfa7058bdaa9 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 @@ -14,12 +14,13 @@ import net.mullvad.mullvadvpn.compose.communication.CustomListAction import net.mullvad.mullvadvpn.compose.communication.CustomListResult import net.mullvad.mullvadvpn.compose.state.UpdateCustomListUiState import net.mullvad.mullvadvpn.model.CustomListsError -import net.mullvad.mullvadvpn.model.UpdateCustomListResult -import net.mullvad.mullvadvpn.repository.CustomListsRepository +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase +import net.mullvad.mullvadvpn.usecase.customlists.CustomListsException class EditCustomListNameDialogViewModel( - private val action: CustomListAction.Rename, - private val customListsRepository: CustomListsRepository + private val customListId: String, + private val initialName: String, + private val customListActionUseCase: CustomListActionUseCase ) : ViewModel() { private val _uiSideEffect = @@ -30,37 +31,35 @@ class EditCustomListNameDialogViewModel( val uiState = _error - .map { UpdateCustomListUiState(name = action.name, error = it) } + .map { UpdateCustomListUiState(name = initialName, error = it) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(), - UpdateCustomListUiState(name = action.name) + UpdateCustomListUiState(name = initialName) ) fun updateCustomListName(name: String) { viewModelScope.launch { - when ( - val result = - customListsRepository.updateCustomListName( - id = action.customListId, - name = name + customListActionUseCase + .performAction( + CustomListAction.Rename( + customListId = customListId, + name = initialName, + newName = name ) - ) { - UpdateCustomListResult.Ok -> { - _uiSideEffect.send( - EditCustomListNameDialogSideEffect.ReturnResult( - result = - CustomListResult.ListRenamed( - name = name, - reverseAction = action.not() - ) - ) - ) - } - is UpdateCustomListResult.Error -> { - _error.emit(result.error) - } - } + ) + .fold( + onSuccess = { result -> + _uiSideEffect.send(EditCustomListNameDialogSideEffect.ReturnResult(result)) + }, + onFailure = { exception -> + if (exception is CustomListsException) { + _error.emit(exception.error) + } else { + _error.emit(CustomListsError.OtherError) + } + } + ) } } @@ -70,6 +69,6 @@ class EditCustomListNameDialogViewModel( } sealed interface EditCustomListNameDialogSideEffect { - data class ReturnResult(val result: CustomListResult.ListRenamed) : + data class ReturnResult(val result: CustomListResult.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 29cbe29af55e..2923b9e1d89d 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 @@ -12,27 +12,27 @@ 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.CustomListResult import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState import net.mullvad.mullvadvpn.compose.state.toNullableOwnership import net.mullvad.mullvadvpn.compose.state.toSelectedProviders import net.mullvad.mullvadvpn.model.Constraint -import net.mullvad.mullvadvpn.model.CreateCustomListResult import net.mullvad.mullvadvpn.model.Ownership import net.mullvad.mullvadvpn.relaylist.Provider import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.relaylist.filterOnSearchTerm import net.mullvad.mullvadvpn.relaylist.toLocationConstraint -import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy import net.mullvad.mullvadvpn.usecase.RelayListFilterUseCase import net.mullvad.mullvadvpn.usecase.RelayListUseCase +import net.mullvad.mullvadvpn.usecase.customlists.CustomListActionUseCase class SelectLocationViewModel( private val serviceConnectionManager: ServiceConnectionManager, private val relayListUseCase: RelayListUseCase, private val relayListFilterUseCase: RelayListFilterUseCase, - private val customListsRepository: CustomListsRepository + private val customListActionUseCase: CustomListActionUseCase ) : ViewModel() { private val _searchTerm = MutableStateFlow(EMPTY_SEARCH_TERM) @@ -128,60 +128,19 @@ class SelectLocationViewModel( fun addLocationToList(item: RelayItem, customList: RelayItem.CustomList) { viewModelScope.launch { - val newLocations = customList.locations + item - customListsRepository.updateCustomListLocations( - id = customList.id, - locations = newLocations - ) - _uiSideEffect.send( - SelectLocationSideEffect.LocationAddedToCustomList( - item, - customList.copy(locations = newLocations) + val newLocations = (customList.locations + item).map { it.code } + val result = + customListActionUseCase.performAction( + CustomListAction.UpdateLocations(customList.id, newLocations) ) - ) - } - } - - fun removeLocationFromList(item: RelayItem, customList: RelayItem.CustomList) { - viewModelScope.launch { - customListsRepository.updateCustomListLocations( - customList.id, - customList.locations - item + _uiSideEffect.send( + SelectLocationSideEffect.LocationAddedToCustomList(result.getOrThrow()) ) } } fun performAction(action: CustomListAction) { - when (action) { - is CustomListAction.Create -> createCustomList(action.name, action.locations) - is CustomListAction.Delete -> deleteCustomList(action.customListId) - is CustomListAction.Rename -> renameCustomList(action.customListId, action.name) - is CustomListAction.UpdateLocations -> - updateLocations(action.customListId, action.locations) - } - } - - private fun createCustomList(name: String, locations: List) { - viewModelScope.launch { - val result = customListsRepository.createCustomList(name) - if (result is CreateCustomListResult.Ok) { - customListsRepository.updateCustomListLocationsFromCodes(result.id, locations) - } - } - } - - private fun deleteCustomList(id: String) { - customListsRepository.deleteCustomList(id) - } - - private fun renameCustomList(id: String, name: String) { - viewModelScope.launch { customListsRepository.updateCustomListName(id, name) } - } - - private fun updateLocations(id: String, locations: List) { - viewModelScope.launch { - customListsRepository.updateCustomListLocationsFromCodes(id, locations) - } + viewModelScope.launch { customListActionUseCase.performAction(action) } } companion object { @@ -192,8 +151,6 @@ class SelectLocationViewModel( sealed interface SelectLocationSideEffect { data object CloseScreen : SelectLocationSideEffect - data class LocationAddedToCustomList( - val item: RelayItem, - val customList: RelayItem.CustomList - ) : SelectLocationSideEffect + data class LocationAddedToCustomList(val result: CustomListResult.LocationsChanged) : + SelectLocationSideEffect }