android/ui: add transfer ID to outgoing file transfers

Helps track status of transfers.

Updates #ENG-2868

Signed-off-by: Percy Wegmann <percy@tailscale.com>
ox/taildrop
Percy Wegmann 2 months ago
parent 29bed882f7
commit 0e1f675375
No known key found for this signature in database
GPG Key ID: 29D8CDEB4C13D48B

@ -47,6 +47,12 @@ import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.localapi.Request import com.tailscale.ipn.ui.localapi.Request
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import libtailscale.Libtailscale
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
@ -54,15 +60,9 @@ import java.net.NetworkInterface
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.util.Locale import java.util.Locale
import java.util.Objects import java.util.Objects
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import libtailscale.Libtailscale
class App : Application(), libtailscale.AppContext { class App : Application(), libtailscale.AppContext {
private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
companion object { companion object {
const val STATUS_CHANNEL_ID = "tailscale-status" const val STATUS_CHANNEL_ID = "tailscale-status"

@ -12,7 +12,7 @@ import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.tailscale.ipn.ui.model.FileTransfer import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.theme.AppTheme import com.tailscale.ipn.ui.theme.AppTheme
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
@ -24,11 +24,13 @@ import kotlinx.coroutines.flow.StateFlow
class ShareActivity : ComponentActivity() { class ShareActivity : ComponentActivity() {
private val TAG = ShareActivity::class.simpleName private val TAG = ShareActivity::class.simpleName
private val transfers: StateFlow<List<FileTransfer>> = MutableStateFlow(emptyList()) private val requestedTransfers: StateFlow<List<Ipn.OutgoingFile>> = MutableStateFlow(emptyList())
override fun onCreate(state: Bundle?) { override fun onCreate(state: Bundle?) {
super.onCreate(state) super.onCreate(state)
setContent { AppTheme { TaildropView(transfers) } } setContent {
AppTheme { TaildropView(requestedTransfers, (application as App).applicationScope) }
}
} }
override fun onStart() { override fun onStart() {
@ -80,7 +82,7 @@ class ShareActivity : ComponentActivity() {
} }
} }
val pendingFiles: List<FileTransfer> = val pendingFiles: List<Ipn.OutgoingFile> =
uris?.filterNotNull()?.mapNotNull { uris?.filterNotNull()?.mapNotNull {
contentResolver?.query(it, null, null, null, null)?.let { c -> contentResolver?.query(it, null, null, null, null)?.let { c ->
val nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME) val nameCol = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
@ -89,7 +91,9 @@ class ShareActivity : ComponentActivity() {
val name = c.getString(nameCol) val name = c.getString(nameCol)
val size = c.getLong(sizeCol) val size = c.getLong(sizeCol)
c.close() c.close()
FileTransfer(name, size, it) val file = Ipn.OutgoingFile(Name = name, DeclaredSize = size)
file.uri = it
file
} }
} ?: emptyList() } ?: emptyList()
@ -97,6 +101,6 @@ class ShareActivity : ComponentActivity() {
Log.e(TAG, "Share failure - no files extracted from intent") Log.e(TAG, "Share failure - no files extracted from intent")
} }
transfers.set(pendingFiles) requestedTransfers.set(pendingFiles)
} }
} }

@ -7,17 +7,11 @@ import android.content.Context
import android.util.Log import android.util.Log
import com.tailscale.ipn.ui.model.BugReportID import com.tailscale.ipn.ui.model.BugReportID
import com.tailscale.ipn.ui.model.Errors import com.tailscale.ipn.ui.model.Errors
import com.tailscale.ipn.ui.model.FileTransfer
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.IpnLocal import com.tailscale.ipn.ui.model.IpnLocal
import com.tailscale.ipn.ui.model.IpnState import com.tailscale.ipn.ui.model.IpnState
import com.tailscale.ipn.ui.model.StableNodeID import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.util.InputStreamAdapter import com.tailscale.ipn.ui.util.InputStreamAdapter
import java.net.URLEncoder
import java.nio.charset.Charset
import java.nio.file.Files.delete
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -27,6 +21,9 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import libtailscale.FilePart import libtailscale.FilePart
import java.nio.charset.Charset
import kotlin.reflect.KType
import kotlin.reflect.typeOf
private object Endpoint { private object Endpoint {
const val DEBUG = "debug" const val DEBUG = "debug"
@ -128,12 +125,10 @@ class Client(private val scope: CoroutineScope) {
fun putTaildropFiles( fun putTaildropFiles(
context: Context, context: Context,
peerId: StableNodeID, peerId: StableNodeID,
files: Collection<FileTransfer>, files: Collection<Ipn.OutgoingFile>,
responseHandler: (Result<String>) -> Unit responseHandler: (Result<String>) -> Unit
) { ) {
val manifest = val manifest = Json.encodeToString(files)
Json.encodeToString(
files.map { URLEncoder.encode(it.filename, "utf-8") to it.size }.toMap())
val manifestPart = FilePart() val manifestPart = FilePart()
manifestPart.body = InputStreamAdapter(manifest.byteInputStream(Charset.defaultCharset())) manifestPart.body = InputStreamAdapter(manifest.byteInputStream(Charset.defaultCharset()))
manifestPart.filename = "manifest.json" manifestPart.filename = "manifest.json"
@ -143,14 +138,13 @@ class Client(private val scope: CoroutineScope) {
try { try {
parts.addAll( parts.addAll(
files.map { file -> files.map { file ->
val stream = context.contentResolver.openInputStream(file.uri) val stream =
if (stream == null) { context.contentResolver.openInputStream(file.uri)
throw Exception("Error opening file stream") ?: throw Exception("Error opening file stream")
}
val part = FilePart() val part = FilePart()
part.filename = URLEncoder.encode(file.filename, "utf-8") part.filename = file.Name
part.contentLength = file.size part.contentLength = file.DeclaredSize
part.body = InputStreamAdapter(stream) part.body = InputStreamAdapter(stream)
part part
}) })

@ -1,9 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.model
import android.net.Uri
// Encapsulates a uri based file transfer for Taildrop.
data class FileTransfer(val filename: String, val size: Long, val uri: Uri)

@ -3,7 +3,10 @@
package com.tailscale.ipn.ui.model package com.tailscale.ipn.ui.model
import android.net.Uri
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.util.UUID
class Ipn { class Ipn {
@ -158,16 +161,25 @@ class Ipn {
@Serializable @Serializable
data class OutgoingFile( data class OutgoingFile(
val ID: String = "",
val Name: String, val Name: String,
val PeerID: StableNodeID, val PeerID: StableNodeID = "",
val Started: String, val Started: String = "",
val DeclaredSize: Long, val DeclaredSize: Long,
val Sent: Long, val Sent: Long = 0L,
val PartialPath: String? = null, val PartialPath: String? = null,
var FinalPath: String? = null, var FinalPath: String? = null,
val Finished: Boolean, val Finished: Boolean = false,
val Succeeded: Boolean, val Succeeded: Boolean = false,
) ) {
@Transient lateinit var uri: Uri // only used on client
fun prepare(peerId: StableNodeID): OutgoingFile {
val f = copy(ID = UUID.randomUUID().toString(), PeerID = peerId)
f.uri = uri
return f
}
}
@Serializable data class FileTarget(var Node: Tailcfg.Node, var PeerAPIURL: String) @Serializable data class FileTarget(var Node: Tailcfg.Node, var PeerAPIURL: String)
} }

@ -28,18 +28,20 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.model.FileTransfer
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.viewModel.TaildropViewModel import com.tailscale.ipn.ui.viewModel.TaildropViewModel
import com.tailscale.ipn.ui.viewModel.TaildropViewModelFactory import com.tailscale.ipn.ui.viewModel.TaildropViewModelFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@Composable @Composable
fun TaildropView( fun TaildropView(
transfersIn: StateFlow<List<FileTransfer>>, requestedTransfers: StateFlow<List<Ipn.OutgoingFile>>,
viewModel: TaildropViewModel = viewModel(factory = TaildropViewModelFactory(transfersIn)) applicationScope: CoroutineScope,
viewModel: TaildropViewModel =
viewModel(factory = TaildropViewModelFactory(requestedTransfers, applicationScope))
) { ) {
Scaffold(topBar = { Header(R.string.share) }) { paddingInsets -> Scaffold(topBar = { Header(R.string.share) }) { paddingInsets ->
val showDialog = viewModel.showDialog.collectAsState().value val showDialog = viewModel.showDialog.collectAsState().value
@ -48,13 +50,12 @@ fun TaildropView(
showDialog?.let { ErrorDialog(type = it, action = { viewModel.showDialog.set(null) }) } showDialog?.let { ErrorDialog(type = it, action = { viewModel.showDialog.set(null) }) }
Column(modifier = Modifier.padding(paddingInsets)) { Column(modifier = Modifier.padding(paddingInsets)) {
val transfers = viewModel.transfers.collectAsState().value FileShareHeader(
val state = viewModel.state.collectAsState().value fileTransfers = requestedTransfers.collectAsState().value,
totalSize = viewModel.totalSize)
FileShareHeader(fileTransfers = transfers, totalSize = viewModel.totalSize)
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
when (state) { when (viewModel.state.collectAsState().value) {
Ipn.State.Running -> { Ipn.State.Running -> {
val peers = viewModel.myPeers.collectAsState().value val peers = viewModel.myPeers.collectAsState().value
val context = LocalContext.current val context = LocalContext.current
@ -129,7 +130,7 @@ fun FileShareConnectView(onToggle: () -> Unit) {
} }
@Composable @Composable
fun FileShareHeader(fileTransfers: List<FileTransfer>, totalSize: Long) { fun FileShareHeader(fileTransfers: List<Ipn.OutgoingFile>, totalSize: Long) {
Column(modifier = Modifier.padding(horizontal = 8.dp)) { Column(modifier = Modifier.padding(horizontal = 8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
IconForTransfer(fileTransfers) IconForTransfer(fileTransfers)
@ -142,7 +143,7 @@ fun FileShareHeader(fileTransfers: List<FileTransfer>, totalSize: Long) {
false -> { false -> {
when (fileTransfers.size) { when (fileTransfers.size) {
1 -> Text(fileTransfers[0].filename, style = MaterialTheme.typography.titleMedium) 1 -> Text(fileTransfers[0].Name, style = MaterialTheme.typography.titleMedium)
else -> else ->
Text( Text(
stringResource(R.string.file_count, fileTransfers.size), stringResource(R.string.file_count, fileTransfers.size),
@ -159,7 +160,7 @@ fun FileShareHeader(fileTransfers: List<FileTransfer>, totalSize: Long) {
} }
@Composable @Composable
fun IconForTransfer(transfers: List<FileTransfer>) { fun IconForTransfer(transfers: List<Ipn.OutgoingFile>) {
// (jonathan) TODO: Thumbnails? // (jonathan) TODO: Thumbnails?
when (transfers.size) { when (transfers.size) {
0 -> 0 ->

@ -18,27 +18,33 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.R import com.tailscale.ipn.R
import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.FileTransfer
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.StableNodeID
import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
import com.tailscale.ipn.ui.view.ActivityIndicator import com.tailscale.ipn.ui.view.ActivityIndicator
import com.tailscale.ipn.ui.view.CheckedIndicator import com.tailscale.ipn.ui.view.CheckedIndicator
import com.tailscale.ipn.ui.view.ErrorDialogType import com.tailscale.ipn.ui.view.ErrorDialogType
import java.net.URLEncoder import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class TaildropViewModelFactory(private val transfers: StateFlow<List<FileTransfer>>) : class TaildropViewModelFactory(
ViewModelProvider.Factory { private val requestedTransfers: StateFlow<List<Ipn.OutgoingFile>>,
private val applicationScope: CoroutineScope
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return TaildropViewModel(transfers) as T return TaildropViewModel(requestedTransfers, applicationScope) as T
} }
} }
class TaildropViewModel(val transfers: StateFlow<List<FileTransfer>>) : IpnViewModel() { class TaildropViewModel(
val requestedTransfers: StateFlow<List<Ipn.OutgoingFile>>,
private val applicationScope: CoroutineScope
) : IpnViewModel() {
// Represents the state of a file transfer // Represents the state of a file transfer
enum class TransferState { enum class TransferState {
@ -50,18 +56,17 @@ class TaildropViewModel(val transfers: StateFlow<List<FileTransfer>>) : IpnViewM
// The overall VPN state // The overall VPN state
val state = Notifier.state val state = Notifier.state
// Map of outgoing files to peer ID. This is the full state of the outgoing files. // Set of all nodes for which we've requested a file transfer. This is used to prevent us from
private val outgoing: StateFlow<Map<String, List<Ipn.OutgoingFile>>> = // request a transfer to the same peer twice.
MutableStateFlow(emptyMap()) private val selectedPeers: StateFlow<Set<StableNodeID>> = MutableStateFlow(emptySet())
// Set of OutgoingFile.IDs that we're currently transferring.
// List of any nodes that have a file transfer pending FOR THE CURRENT SESSION private val currentTransferIDs: StateFlow<Set<String>> = MutableStateFlow(emptySet())
// This is used to filter outgoingFiles to ensure we only render the transfer state // Flow of Ipn.OutgoingFiles with updated statuses for every entry in transferWithStatuses.
// for things that are currently displayed. private val transfers: StateFlow<List<Ipn.OutgoingFile>> = MutableStateFlow(emptyList())
private val pending: StateFlow<Set<String>> = MutableStateFlow(emptySet())
// The total size of all pending files. // The total size of all pending files.
var totalSize: Long = 0 val totalSize: Long
get() = transfers.value.sumOf { it.size } get() = requestedTransfers.value.sumOf { it.DeclaredSize }
// The list of peers that we can share with. This includes only the nodes belonging to the user // The list of peers that we can share with. This includes only the nodes belonging to the user
// and excludes the current node. Sorted by online devices first, and offline second, // and excludes the current node. Sorted by online devices first, and offline second,
@ -83,35 +88,27 @@ class TaildropViewModel(val transfers: StateFlow<List<FileTransfer>>) : IpnViewM
viewModelScope.launch { viewModelScope.launch {
// Map the outgoing files by their PeerId since we need to display them for each peer // Map the outgoing files by their PeerId since we need to display them for each peer
// We only need to track files which are pending send, everything else is irrelevant. // We only need to track files which are pending send, everything else is irrelevant.
Notifier.outgoingFiles.collect { outgoingFiles -> Notifier.outgoingFiles
val outgoingMap: MutableMap<String, List<Ipn.OutgoingFile>> = mutableMapOf() .combine(currentTransferIDs) { outgoingFiles, ongoingIDs ->
val currentFiles = transfers.value.map { URLEncoder.encode(it.filename, "utf-8") } Pair(outgoingFiles, ongoingIDs)
}
outgoingFiles?.let { files -> .collect { (outgoingFiles, ongoingIDs) ->
files outgoingFiles?.let {
.filter { currentFiles.contains(it.Name) && pending.value.contains(it.PeerID) } transfers.set(outgoingFiles.filter { ongoingIDs.contains(it.ID) })
.forEach { } ?: run { transfers.set(emptyList()) }
val list = outgoingMap.getOrDefault(it.PeerID, emptyList()).toMutableList() }
list += it
outgoingMap[it.PeerID] = list
}
Log.d("TaildropViewModel", "Outgoing files: $outgoingMap")
outgoing.set(outgoingMap)
} ?: run { outgoing.set(emptyMap()) }
}
} }
// Whenever our files list changes, we need to reset the outgoings files map and
// any pending requests. The user has changed the files they're attempting to share.
viewModelScope.launch { viewModelScope.launch {
transfers.collect { requestedTransfers.collect {
pending.set(emptySet()) // This means that we're processing a new share intent, clear current state
outgoing.set(emptyMap()) selectedPeers.set(emptySet())
currentTransferIDs.set(emptySet())
} }
} }
} }
// Calculates the overall progress for a set of outoing files // Calculates the overall progress for a set of outgoing files
private fun progress(transfers: List<Ipn.OutgoingFile>): Double { private fun progress(transfers: List<Ipn.OutgoingFile>): Double {
val total = transfers.sumOf { it.DeclaredSize }.toDouble() val total = transfers.sumOf { it.DeclaredSize }.toDouble()
val sent = transfers.sumOf { it.Sent }.toDouble() val sent = transfers.sumOf { it.Sent }.toDouble()
@ -122,15 +119,10 @@ class TaildropViewModel(val transfers: StateFlow<List<FileTransfer>>) : IpnViewM
// Calculates the overall state of a set of file transfers. // Calculates the overall state of a set of file transfers.
// peerId: The peer ID to check for transfers. // peerId: The peer ID to check for transfers.
// transfers: The list of outgoing file transfers for the peer. // transfers: The list of outgoing file transfers for the peer.
private fun transferState(peerId: String, transfers: List<Ipn.OutgoingFile>): TransferState? { private fun transferState(transfers: List<Ipn.OutgoingFile>): TransferState? {
// No transfers? Nothing state // No transfers? Nothing state
if (transfers.isEmpty()) return null if (transfers.isEmpty()) return null
// We may have transfers from a prior session for files the user selected and for peers
// in our list.. but we don't care about those. We only care if the peerId is in teh pending
// list.
if (!pending.value.contains(peerId)) return null
return if (transfers.all { it.Finished }) { return if (transfers.all { it.Finished }) {
// Everything done? SENT if all succeeded, FAILED if any failed. // Everything done? SENT if all succeeded, FAILED if any failed.
if (transfers.any { !it.Succeeded }) TransferState.FAILED else TransferState.SENT if (transfers.any { !it.Succeeded }) TransferState.FAILED else TransferState.SENT
@ -159,21 +151,11 @@ class TaildropViewModel(val transfers: StateFlow<List<FileTransfer>>) : IpnViewM
// any requested transfers. // any requested transfers.
@Composable @Composable
fun TrailingContentForPeer(peerId: String) { fun TrailingContentForPeer(peerId: String) {
val outgoing = outgoing.collectAsState().value
val pending = pending.collectAsState().value
// Check our outgoing files for the peer and determine the state of the transfer. // Check our outgoing files for the peer and determine the state of the transfer.
val transfers = outgoing[peerId] ?: emptyList() val transfers = this.transfers.collectAsState().value.filter { it.PeerID == peerId }
var status = transferState(peerId, transfers) var status: TransferState = transferState(transfers) ?: return
// Check if we have a pending transfer for this peer. We may not have an outgoing file
// yet, but we still want to show the sending state in the mean time.
if (status == null && pending.contains(peerId)) {
status = TransferState.SENDING
}
// Still no status? Nothing to render for this peer // Still no status? Nothing to render for this peer
if (status == null) return
Column(modifier = Modifier.fillMaxHeight()) { Column(modifier = Modifier.fillMaxHeight()) {
when (status) { when (status) {
@ -197,20 +179,20 @@ class TaildropViewModel(val transfers: StateFlow<List<FileTransfer>>) : IpnViewM
return return
} }
// Ignore requests to resend a file (the backend will not overwrite anyway) if (selectedPeers.value.contains(node.StableID)) {
outgoing.value[node.StableID]?.let { // We've already selected this peer, ignore
val status = transferState(node.StableID, it) return
if (status == TransferState.SENDING || status == TransferState.SENT) {
return
}
} }
selectedPeers.set(selectedPeers.value + node.StableID)
val preparedTransfers = requestedTransfers.value.map { it.prepare(node.StableID) }
currentTransferIDs.set(currentTransferIDs.value + preparedTransfers.map { it.ID })
pending.set(pending.value + node.StableID) Client(applicationScope).putTaildropFiles(context, node.StableID, preparedTransfers) {
Client(viewModelScope).putTaildropFiles(context, node.StableID, transfers.value) {
// This is an early API failure and will not get communicated back up to us via // This is an early API failure and will not get communicated back up to us via
// outgoing files - things never made it that far. // outgoing files - things never made it that far.
if (it.isFailure) { if (it.isFailure) {
pending.set(pending.value - node.StableID) selectedPeers.set(selectedPeers.value - node.StableID)
showDialog.set(ErrorDialogType.SHARE_FAILED) showDialog.set(ErrorDialogType.SHARE_FAILED)
} }
} }

@ -13,7 +13,7 @@ require (
golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87 golang.org/x/mobile v0.0.0-20240319015410-c58ccf4b0c87
golang.org/x/sys v0.18.0 golang.org/x/sys v0.18.0
inet.af/netaddr v0.0.0-20220617031823-097006376321 inet.af/netaddr v0.0.0-20220617031823-097006376321
tailscale.com v1.63.0-pre.0.20240324181545-f78928191539 tailscale.com v1.63.0-pre.0.20240326143037-34ae89e4972c
) )
require ( require (

@ -666,5 +666,5 @@ sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
tailscale.com v1.63.0-pre.0.20240324181545-f78928191539 h1:DBpvudmMiWINWePS9RdY+dST7xOideFf26TSgqsu2rQ= tailscale.com v1.63.0-pre.0.20240326143037-34ae89e4972c h1:j7pSWQa+9OdNavdMVq0ZN7RDQ7CZDIxzTveKSeZ7I0c=
tailscale.com v1.63.0-pre.0.20240324181545-f78928191539/go.mod h1:cC0b0vYCoSDOLufJX5J5zDUrvV3lYwOLqlt9NW8y4cY= tailscale.com v1.63.0-pre.0.20240326143037-34ae89e4972c/go.mod h1:cC0b0vYCoSDOLufJX5J5zDUrvV3lYwOLqlt9NW8y4cY=

Loading…
Cancel
Save