Recover join dialog after clipboard and connection errors

This commit is contained in:
evgeny committed 2026-10-29 13:34:25 +01:00
1 parent 87b7c6f36c
commit bd4bd47e00
3 files changed
+51 -22

No files matched your search

@@ -1,7 +1,6 @@
package com.utun.chat.ui.screens
import com.utun.chat.R
import com.utun.chat.data.AppLanguage
import androidx.compose.ui.res.stringResource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -10,42 +9,65 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.unit.dp
import com.utun.chat.data.InviteData
import com.utun.chat.data.InviteLink
import com.utun.chat.data.LogManager
import kotlinx.coroutines.CancellationException
/** Автоматически передаёт валидную ссылку из ввода/QR/буфера в onConnect; сетевой результат показывает вызывающий экран.
* Callback должен принять запрос и закрыть диалог. Ошибка запуска разрешает повторить ту же ссылку. Секреты ссылки не логируются. */
@Composable
fun JoinChannelDialog(onConnect: (InviteData) -> Unit, onDismiss: () -> Unit) {
val clipboard = LocalClipboardManager.current
var link by remember { mutableStateOf("") }
var error by remember { mutableStateOf<String?>(null) }
var link by rememberSaveable { mutableStateOf("") }
var error by remember { mutableStateOf<Int?>(null) }
var connecting by remember { mutableStateOf(false) }
var retry by remember { mutableIntStateOf(0) }
val connect by rememberUpdatedState(onConnect)
LaunchedEffect(Unit) {
val text = clipboard.getText()?.text?.trim().orEmpty()
if (text.startsWith("utun://")) {
LogManager.addLog("INFO", "INVITE", "Invite link pasted from clipboard")
link = text
fun paste(onlyInvite: Boolean) {
try {
val text = clipboard.getText()?.text?.trim().orEmpty()
if (!onlyInvite || text.startsWith("utun://")) {
LogManager.addLog("INFO", "INVITE", "Invite input read from clipboard")
link = text
}
} catch (e: Exception) {
LogManager.addLog("ERROR", "INVITE", "Clipboard read failed: $e")
error = R.string.invite_paste_failed
}
}
LaunchedEffect(link) {
error = null
LaunchedEffect(Unit) {
if (link.isEmpty()) paste(onlyInvite = true)
}
LaunchedEffect(link, retry) {
val text = link.trim()
if (text.isEmpty() || connecting) return@LaunchedEffect
val invite = InviteLink.decode(text)
if (invite == null) {
LogManager.addLog("WARN", "INVITE", "Invalid invite link in join dialog")
error = AppLanguage.text(R.string.invalid_invite_link)
} else {
connecting = true
LogManager.addLog("INFO", "INVITE", "connect: ch=0x%016x node=0x%016x addrs=%d"
.format(invite.channelId, invite.nodeId, invite.addrs.size))
connect(invite)
error = null
try {
val invite = InviteLink.decode(text)
if (invite == null) {
LogManager.addLog("WARN", "INVITE", "Invalid invite link in join dialog")
error = R.string.invalid_invite_link
} else {
connecting = true
LogManager.addLog("INFO", "INVITE", "connect: ch=0x%016x node=0x%016x addrs=%d"
.format(invite.channelId, invite.nodeId, invite.addrs.size))
connect(invite)
}
} catch (e: CancellationException) {
connecting = false
throw e
} catch (e: Exception) {
LogManager.addLog("ERROR", "INVITE", "Invite connection request failed: ${e.javaClass.simpleName}")
connecting = false
error = R.string.invite_connect_failed
}
}
@@ -65,11 +87,11 @@ fun JoinChannelDialog(onConnect: (InviteData) -> Unit, onDismiss: () -> Unit) {
}
OutlinedTextField(
value = link,
onValueChange = { link = it },
onValueChange = { error = null; link = it },
label = { Text(stringResource(R.string.invite_link)) },
placeholder = { Text("utun://...") },
trailingIcon = {
IconButton(onClick = { link = clipboard.getText()?.text.orEmpty() }, enabled = !connecting) {
IconButton(onClick = { paste(onlyInvite = false) }, enabled = !connecting) {
Icon(Icons.Filled.ContentPaste, stringResource(R.string.paste_invite_link))
}
},
@@ -78,7 +100,10 @@ fun JoinChannelDialog(onConnect: (InviteData) -> Unit, onDismiss: () -> Unit) {
isError = error != null,
modifier = Modifier.fillMaxWidth()
)
error?.let { Text(it, color = MaterialTheme.colorScheme.error) }
error?.let { Text(stringResource(it), color = MaterialTheme.colorScheme.error) }
if (error == R.string.invite_connect_failed) {
TextButton(onClick = { retry++ }) { Text(stringResource(R.string.retry)) }
}
if (connecting) Text(stringResource(R.string.connecting_peer), color = MaterialTheme.colorScheme.primary)
}
},
@@ -161,6 +161,8 @@
<string name="qr_code">"QR-код"</string>
<string name="invite_build_failed">"Не удалось собрать ссылку"</string>
<string name="invite_copy_failed">Не удалось скопировать ссылку-приглашение</string>
<string name="invite_paste_failed">Не удалось прочитать буфер обмена. Введите ссылку вручную.</string>
<string name="invite_connect_failed">Не удалось начать подключение. Повторите попытку.</string>
<string name="invite_delivery_status">%1$s: %2$s</string>
<string name="copied">"Скопировано!"</string>
<string name="copy_link">"Копировать ссылку"</string>
@@ -162,6 +162,8 @@
<string name="qr_code">"QR code"</string>
<string name="invite_build_failed">"Could not build invite link"</string>
<string name="invite_copy_failed">Could not copy the invite link</string>
<string name="invite_paste_failed">Could not read the clipboard. Enter an invite link manually.</string>
<string name="invite_connect_failed">Could not start connecting. Please try again.</string>
<string name="invite_delivery_status">%1$s: %2$s</string>
<string name="copied">"Copied!"</string>
<string name="copy_link">"Copy link"</string>