ExitNodePicker: don't allow run as exit node while using exit node (#411)

Updates tailscale/corp#19122

Signed-off-by: kari-ts <kari@tailscale.com>
pull/413/head
kari-ts 6 months ago committed by GitHub
parent 75db9e64c8
commit 0df6c61eee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,8 +5,8 @@ package com.tailscale.ipn.ui.view
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check import androidx.compose.material.icons.outlined.Check
@ -18,9 +18,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@ -37,13 +34,14 @@ import com.tailscale.ipn.ui.viewModel.ExitNodePickerNav
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModel
import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModelFactory import com.tailscale.ipn.ui.viewModel.ExitNodePickerViewModelFactory
import com.tailscale.ipn.ui.viewModel.selected import com.tailscale.ipn.ui.viewModel.selected
import kotlinx.coroutines.flow.MutableStateFlow
@Composable @Composable
fun ExitNodePicker( fun ExitNodePicker(
nav: ExitNodePickerNav, nav: ExitNodePickerNav,
model: ExitNodePickerViewModel = viewModel(factory = ExitNodePickerViewModelFactory(nav)) model: ExitNodePickerViewModel = viewModel(factory = ExitNodePickerViewModelFactory(nav))
) { ) {
LoadingIndicator.Wrap { LoadingIndicator.Wrap {
Scaffold(topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateBackHome) }) { Scaffold(topBar = { Header(R.string.choose_exit_node, onBack = nav.onNavigateBackHome) }) {
innerPadding -> innerPadding ->
val tailnetExitNodes by model.tailnetExitNodes.collectAsState() val tailnetExitNodes by model.tailnetExitNodes.collectAsState()
@ -60,13 +58,13 @@ fun ExitNodePicker(
model, model,
ExitNodePickerViewModel.ExitNode( ExitNodePickerViewModel.ExitNode(
label = stringResource(R.string.none), label = stringResource(R.string.none),
online = true, online = MutableStateFlow(true),
selected = !anyActive, selected = !anyActive,
)) ))
if (showRunAsExitNode == ShowHide.Show) { if (showRunAsExitNode == ShowHide.Show) {
Lists.ItemDivider() Lists.ItemDivider()
RunAsExitNodeItem(nav = nav, viewModel = model) RunAsExitNodeItem(nav = nav, viewModel = model, anyActive)
} }
} }
@ -102,17 +100,18 @@ fun ExitNodeItem(
viewModel: ExitNodePickerViewModel, viewModel: ExitNodePickerViewModel,
node: ExitNodePickerViewModel.ExitNode, node: ExitNodePickerViewModel.ExitNode,
) { ) {
val online by rememberUpdatedState(newValue = node.online) val online by node.online.collectAsState()
val isRunningExitNode = viewModel.isRunningExitNode.collectAsState().value
Box { Box {
var modifier: Modifier = Modifier var modifier: Modifier = Modifier
if (online) { if (online && !isRunningExitNode) {
modifier = modifier.clickable { viewModel.setExitNode(node) } modifier = modifier.clickable { viewModel.setExitNode(node) }
} }
ListItem( ListItem(
modifier = modifier, modifier = modifier,
colors = colors =
if (online) MaterialTheme.colorScheme.listItem if (online && !isRunningExitNode) MaterialTheme.colorScheme.listItem
else MaterialTheme.colorScheme.disabledListItem, else MaterialTheme.colorScheme.disabledListItem,
headlineContent = { headlineContent = {
Text(node.city.ifEmpty { node.label }, style = MaterialTheme.typography.bodyMedium) Text(node.city.ifEmpty { node.label }, style = MaterialTheme.typography.bodyMedium)
@ -155,12 +154,19 @@ fun MullvadItem(nav: ExitNodePickerNav, count: Int, selected: Boolean) {
} }
@Composable @Composable
fun RunAsExitNodeItem(nav: ExitNodePickerNav, viewModel: ExitNodePickerViewModel) { fun RunAsExitNodeItem(nav: ExitNodePickerNav, viewModel: ExitNodePickerViewModel, anyActive: Boolean) {
val isRunningExitNode = viewModel.isRunningExitNode.collectAsState().value val isRunningExitNode = viewModel.isRunningExitNode.collectAsState().value
Box { Box {
var modifier: Modifier = Modifier
if (!anyActive) {
modifier = modifier.clickable { nav.onNavigateToRunAsExitNode() }
}
ListItem( ListItem(
modifier = Modifier.clickable { nav.onNavigateToRunAsExitNode() }, modifier = modifier,
colors =
if (!anyActive) MaterialTheme.colorScheme.listItem
else MaterialTheme.colorScheme.disabledListItem,
headlineContent = { headlineContent = {
Text( Text(
stringResource(id = R.string.run_as_exit_node), stringResource(id = R.string.run_as_exit_node),

@ -39,7 +39,7 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
data class ExitNode( data class ExitNode(
val id: StableNodeID? = null, val id: StableNodeID? = null,
val label: String, val label: String,
val online: Boolean, val online: StateFlow<Boolean>,
val selected: Boolean, val selected: Boolean,
val mullvad: Boolean = false, val mullvad: Boolean = false,
val priority: Int = 0, val priority: Int = 0,
@ -70,7 +70,7 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
ExitNode( ExitNode(
id = it.StableID, id = it.StableID,
label = it.displayName, label = it.displayName,
online = it.Online ?: false, online = MutableStateFlow(it.Online ?: false),
selected = it.StableID == exitNodeId, selected = it.StableID == exitNodeId,
mullvad = it.Name.endsWith(".mullvad.ts.net."), mullvad = it.Name.endsWith(".mullvad.ts.net."),
priority = it.Hostinfo.Location?.Priority ?: 0, priority = it.Hostinfo.Location?.Priority ?: 0,
@ -84,9 +84,10 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
tailnetExitNodes.set(tailnetNodes.sortedWith { a, b -> a.label.compareTo(b.label) }) tailnetExitNodes.set(tailnetNodes.sortedWith { a, b -> a.label.compareTo(b.label) })
val allMullvadExitNodes = val allMullvadExitNodes =
allNodes.filter { allNodes.filter { node ->
// Pick all mullvad nodes that are online or the currently selected // Pick all mullvad nodes that are online or the currently selected
it.mullvad && (it.selected || it.online) val online = node.online.value
node.mullvad && (node.selected || online)
} }
val mullvadExitNodes = val mullvadExitNodes =
allMullvadExitNodes allMullvadExitNodes

Loading…
Cancel
Save