Skip to content

Commit

Permalink
Simplify ViewModel and take setting once
Browse files Browse the repository at this point in the history
  • Loading branch information
Rawa committed Jun 4, 2024
1 parent 45c369f commit 125fc8c
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.ui.test.onNodeWithText
import net.mullvad.mullvadvpn.compose.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewState
import net.mullvad.mullvadvpn.viewmodel.ValidationError
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

Expand All @@ -19,8 +20,8 @@ class DnsDialogTest {

private val defaultState =
DnsDialogViewState(
ipAddress = "",
validationResult = DnsDialogViewState.ValidationResult.Success,
input = "",
validationError = null,
isLocal = false,
isAllowLanEnabled = false,
index = null
Expand Down Expand Up @@ -93,8 +94,8 @@ class DnsDialogTest {
setContentWithTheme {
testDnsDialog(
defaultState.copy(
ipAddress = invalidIpAddress,
validationResult = DnsDialogViewState.ValidationResult.InvalidAddress,
input = invalidIpAddress,
validationError = ValidationError.InvalidAddress,
)
)
}
Expand All @@ -110,8 +111,8 @@ class DnsDialogTest {
setContentWithTheme {
testDnsDialog(
defaultState.copy(
ipAddress = "192.168.0.1",
validationResult = DnsDialogViewState.ValidationResult.DuplicateAddress,
input = "192.168.0.1",
validationError = ValidationError.DuplicateAddress,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,67 +28,26 @@ import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.viewmodel.DnsDialogSideEffect
import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewModel
import net.mullvad.mullvadvpn.viewmodel.DnsDialogViewState
import net.mullvad.mullvadvpn.viewmodel.ValidationError
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf

@Preview
@Composable
private fun PreviewDnsDialogNew() {
AppTheme {
DnsDialog(
DnsDialogViewState(
"1.1.1.1",
DnsDialogViewState.ValidationResult.Success,
false,
false,
null
),
{},
{},
{},
{}
)
}
AppTheme { DnsDialog(DnsDialogViewState("1.1.1.1", null, false, false, null), {}, {}, {}, {}) }
}

@Preview
@Composable
private fun PreviewDnsDialogEdit() {
AppTheme {
DnsDialog(
DnsDialogViewState(
"1.1.1.1",
DnsDialogViewState.ValidationResult.Success,
false,
false,
0
),
{},
{},
{},
{}
)
}
AppTheme { DnsDialog(DnsDialogViewState("1.1.1.1", null, false, false, 0), {}, {}, {}, {}) }
}

@Preview
@Composable
private fun PreviewDnsDialogEditAllowLanDisabled() {
AppTheme {
DnsDialog(
DnsDialogViewState(
"192.168.1.1",
DnsDialogViewState.ValidationResult.Success,
true,
false,
0
),
{},
{},
{},
{}
)
}
AppTheme { DnsDialog(DnsDialogViewState("192.168.1.1", null, true, false, 0), {}, {}, {}, {}) }
}

@Destination(style = DestinationStyle.Dialog::class)
Expand Down Expand Up @@ -143,7 +102,7 @@ fun DnsDialog(
text = {
Column {
DnsTextField(
value = state.ipAddress,
value = state.input,
isValidValue = state.isValid(),
onValueChanged = { newDnsValue -> onDnsInputChange(newDnsValue) },
onSubmit = onSaveDnsClick,
Expand All @@ -154,8 +113,7 @@ fun DnsDialog(

val errorMessage =
when {
state.validationResult is
DnsDialogViewState.ValidationResult.DuplicateAddress -> {
state.validationError is ValidationError.DuplicateAddress -> {
stringResource(R.string.duplicate_address_warning)
}
state.isLocal && !state.isAllowLanEnabled -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package net.mullvad.mullvadvpn.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import java.net.InetAddress
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
Expand All @@ -11,7 +14,6 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand All @@ -25,88 +27,77 @@ sealed interface DnsDialogSideEffect {
data object Error : DnsDialogSideEffect
}

data class DnsDialogViewModelState(
val customDnsList: List<InetAddress>,
val isAllowLanEnabled: Boolean
) {
companion object {
fun default() = DnsDialogViewModelState(emptyList(), false)
}
}

data class DnsDialogViewState(
val ipAddress: String,
val validationResult: ValidationResult = ValidationResult.Success,
val input: String,
val validationError: ValidationError?,
val isLocal: Boolean,
val isAllowLanEnabled: Boolean,
val index: Int?,
) {
val isNewEntry = index == null

fun isValid() = (validationResult is ValidationResult.Success)

sealed class ValidationResult {
data object Success : ValidationResult()
fun isValid() = validationError == null
}

data object InvalidAddress : ValidationResult()
sealed class ValidationError {
data object InvalidAddress : ValidationError()

data object DuplicateAddress : ValidationResult()
}
data object DuplicateAddress : ValidationError()
}

class DnsDialogViewModel(
private val repository: SettingsRepository,
private val inetAddressValidator: InetAddressValidator,
private val index: Int? = null,
index: Int? = null,
initialValue: String?,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : ViewModel() {

private val currentIndex = MutableStateFlow(index)
private val _ipAddressInput = MutableStateFlow(initialValue ?: EMPTY_STRING)

private val vmState =
repository.settingsUpdates
.filterNotNull()
.map {
val customDnsList = it.addresses()
val isAllowLanEnabled = it.allowLan
DnsDialogViewModelState(customDnsList, isAllowLanEnabled = isAllowLanEnabled)
}
.stateIn(viewModelScope, SharingStarted.Lazily, DnsDialogViewModelState.default())

val uiState: StateFlow<DnsDialogViewState> =
combine(_ipAddressInput, vmState, ::createViewState)
combine(_ipAddressInput, currentIndex, repository.settingsUpdates.filterNotNull()) {
input,
currentIndex,
settings ->
createViewState(settings.addresses(), currentIndex, settings.allowLan, input)
}
.stateIn(
viewModelScope,
SharingStarted.Lazily,
createViewState(_ipAddressInput.value, vmState.value)
createViewState(emptyList(), null, false, _ipAddressInput.value)
)

private val _uiSideEffect = Channel<DnsDialogSideEffect>()
val uiSideEffect = _uiSideEffect.receiveAsFlow()

private fun createViewState(ipAddress: String, vmState: DnsDialogViewModelState) =
private fun createViewState(
customDnsList: List<InetAddress>,
currentIndex: Int?,
isAllowLanEnabled: Boolean,
input: String
): DnsDialogViewState =
DnsDialogViewState(
ipAddress,
ipAddress.validateDnsEntry(index, vmState.customDnsList),
ipAddress.isLocalAddress(),
isAllowLanEnabled = vmState.isAllowLanEnabled,
index
input,
input.validateDnsEntry(currentIndex, customDnsList).leftOrNull(),
input.isLocalAddress(),
isAllowLanEnabled = isAllowLanEnabled,
currentIndex
)

private fun String.validateDnsEntry(
index: Int?,
dnsList: List<InetAddress>
): DnsDialogViewState.ValidationResult =
when {
this.isBlank() || !this.isValidIp() -> {
DnsDialogViewState.ValidationResult.InvalidAddress
}
InetAddress.getByName(this).isDuplicateDnsEntry(index, dnsList) -> {
DnsDialogViewState.ValidationResult.DuplicateAddress
}
else -> DnsDialogViewState.ValidationResult.Success
): Either<ValidationError, InetAddress> = either {
ensure(isNotBlank()) { ValidationError.InvalidAddress }
ensure(isValidIp()) { ValidationError.InvalidAddress }
val inetAddress = InetAddress.getByName(this@validateDnsEntry)
ensure(!inetAddress.isDuplicateDnsEntry(index, dnsList)) {
ValidationError.DuplicateAddress
}
inetAddress
}

fun onDnsInputChange(ipAddress: String) {
_ipAddressInput.value = ipAddress
Expand All @@ -116,17 +107,20 @@ class DnsDialogViewModel(
viewModelScope.launch(dispatcher) {
if (!uiState.value.isValid()) return@launch

val address = InetAddress.getByName(uiState.value.ipAddress)
val address = InetAddress.getByName(uiState.value.input)

if (index != null) {
val index = uiState.value.index
val result =
if (index != null) {
repository.setCustomDns(index = index, address = address)
} else {
repository.addCustomDns(address = address)
repository.addCustomDns(address = address).onRight { currentIndex.value = it }
}
.fold(
{ _uiSideEffect.send(DnsDialogSideEffect.Error) },
{ _uiSideEffect.send(DnsDialogSideEffect.Complete) }
)

result.fold(
{ _uiSideEffect.send(DnsDialogSideEffect.Error) },
{ _uiSideEffect.send(DnsDialogSideEffect.Complete) }
)
}

fun onRemoveDnsClick(index: Int) =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,15 @@ class ManagementService(
.mapLeft(SetDnsOptionsError::Unknown)
.mapEmpty()

suspend fun addCustomDns(address: InetAddress): Either<SetDnsOptionsError, Unit> =
suspend fun addCustomDns(address: InetAddress): Either<SetDnsOptionsError, Int> =
Either.catch {
val currentDnsOptions = getSettings().tunnelOptions.dnsOptions
val updatedDnsOptions =
DnsOptions.customOptions.addresses.modify(currentDnsOptions) { it + address }
grpc.setDnsOptions(updatedDnsOptions.fromDomain())
updatedDnsOptions.customOptions.addresses.lastIndex
}
.mapLeft(SetDnsOptionsError::Unknown)
.mapEmpty()

suspend fun deleteCustomDns(index: Int): Either<SetDnsOptionsError, Unit> =
Either.catch {
Expand Down

0 comments on commit 125fc8c

Please sign in to comment.