Report invitation presentation errors without exposing link secrets
This commit is contained in:
1 parent
6f9ddc88e2
commit
237ba93e1a
1 file changed
+38
-9
+38
-9
@@ -36,6 +36,11 @@ private fun candidateLabel(c: InviteCandidate): String =
|
||||
else -> ""
|
||||
}
|
||||
|
||||
/** Показ зарегистрированной ссылки/QR и отправка её устройству в LAN; генерацией и корреляцией link/pending владеет caller.
|
||||
* Первый candidate — визуальный fallback; явный выбор вызывает onSelectNode, начальный запрос делает caller.
|
||||
* При открытии запрашивает LAN-снимок и снимает только прежний терминальный результат; активная отправка переживает закрытие.
|
||||
* Результат LAN относится к сохранённому адресату, не означает JOIN. Состояние копирования сбрасывается при смене ссылки.
|
||||
* Ошибки QR оставляют текст/копирование доступными; ошибки clipboard показываются в диалоге. Содержимое ссылки не логируется. */
|
||||
@Composable
|
||||
fun InviteLinkDialog(
|
||||
candidates: List<InviteCandidate>,
|
||||
@@ -45,7 +50,8 @@ fun InviteLinkDialog(
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var copied by remember { mutableStateOf(false) }
|
||||
var copied by remember(link) { mutableStateOf(false) }
|
||||
var copyFailed by remember(link) { mutableStateOf(false) }
|
||||
var selectedNodeId by remember { mutableStateOf<Long?>(null) }
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
val devices by LocalDiscovery.devices.collectAsState()
|
||||
@@ -53,7 +59,14 @@ fun InviteLinkDialog(
|
||||
var selectedDevice by remember { mutableStateOf<Long?>(null) }
|
||||
var devicesOpen by remember { mutableStateOf(false) }
|
||||
val device = devices.firstOrNull { it.nodeId == selectedDevice } ?: devices.firstOrNull()
|
||||
LaunchedEffect(Unit) { LocalDiscovery.clearDelivery(); NativeLib.requestLocalDevices() }
|
||||
LaunchedEffect(Unit) {
|
||||
LocalDiscovery.clearDelivery()
|
||||
try {
|
||||
if (!NativeLib.requestLocalDevices()) LogManager.addLog("WARN", "INVITE", "LAN device snapshot request rejected")
|
||||
} catch (error: Exception) {
|
||||
LogManager.addLog("ERROR", "INVITE", "LAN device snapshot request failed: ${error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
val effectiveSelected = remember(candidates, selectedNodeId) {
|
||||
candidates.firstOrNull { it.nodeId == selectedNodeId } ?: candidates.firstOrNull()
|
||||
@@ -110,6 +123,7 @@ fun InviteLinkDialog(
|
||||
}
|
||||
else -> Text(stringResource(R.string.invite_build_failed), fontFamily = FontFamily.Monospace, fontSize = 11.sp)
|
||||
}
|
||||
if (copyFailed) Text(stringResource(R.string.invite_copy_failed), color = MaterialTheme.colorScheme.error)
|
||||
if (devices.isNotEmpty()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(stringResource(R.string.local_invite_devices), style = MaterialTheme.typography.bodyMedium)
|
||||
@@ -128,24 +142,37 @@ fun InviteLinkDialog(
|
||||
}) { Text(stringResource(R.string.local_invite_send)) }
|
||||
}
|
||||
delivery?.let { status ->
|
||||
Text(stringResource(when {
|
||||
val text = stringResource(when {
|
||||
status.pending -> R.string.local_invite_sending
|
||||
status.forbidden -> R.string.local_invite_forbidden
|
||||
status.received -> R.string.local_invite_delivered
|
||||
else -> R.string.local_invite_failed
|
||||
}), style = MaterialTheme.typography.bodySmall)
|
||||
})
|
||||
Text(stringResource(R.string.invite_delivery_status, status.device.label, text),
|
||||
style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
enabled = link != null,
|
||||
enabled = link != null && !pending,
|
||||
onClick = {
|
||||
link?.let {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("invite", it))
|
||||
copied = true
|
||||
LogManager.addLog("INFO", "INVITE", "Link copied to clipboard")
|
||||
copyFailed = false; copied = false
|
||||
try {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
||||
if (clipboard == null) {
|
||||
copyFailed = true
|
||||
LogManager.addLog("ERROR", "INVITE", "Clipboard service unavailable")
|
||||
} else {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("invite", it))
|
||||
copied = true
|
||||
LogManager.addLog("INFO", "INVITE", "Link copied to clipboard")
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
copyFailed = true
|
||||
LogManager.addLog("ERROR", "INVITE", "Clipboard copy failed: ${error.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
) { Text(stringResource(if (copied) R.string.copied else R.string.copy_link)) }
|
||||
@@ -156,6 +183,7 @@ fun InviteLinkDialog(
|
||||
)
|
||||
}
|
||||
|
||||
/** QR 400×400 для текущей ссылки; ошибка логируется без секретных данных и оставляет владельцу текстовый fallback. */
|
||||
private fun generateQRCode(text: String): Bitmap? {
|
||||
return try {
|
||||
val writer = MultiFormatWriter()
|
||||
@@ -168,6 +196,7 @@ private fun generateQRCode(text: String): Bitmap? {
|
||||
}
|
||||
bitmap
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "INVITE", "QR generation failed length=${text.length}: ${e.javaClass.simpleName}")
|
||||
null
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user