From 3e246e9e0bd1af7f97916c096aed5c5b21e21199 Mon Sep 17 00:00:00 2001 From: m Date: Fri, 8 May 2026 16:32:48 +1000 Subject: [PATCH] fix(desktop): make error messages selectable so users can copy them Several user-visible error messages on Desktop are rendered with plain `Text` composables, which means they can't be selected or copied. That makes it awkward to share an error in a bug report or paste a hex error string into a search. Wrap the error text in `SelectionContainer` at the canonical sites: - `commons.ui.components.LoadingState`: wrap the description in `EmptyState` and the message in `ErrorState`. `EmptyState` is reused as the in-feed error renderer (e.g. 'Error loading feed' with the underlying error in `description`), so this covers feed/loading errors across screens that use these helpers. - `ComposeNoteDialog`: wrap the validation error and the upload error in the compose-note dialog. - `auth/LoginCard` (Nostr Connect): wrap the connection error. - `auth/KeyInputField`: wrap the supporting-text error so the inline message under the nsec input field can be copied. No visual changes \u2014 `SelectionContainer` does not affect layout or styling. Selection works on Compose Desktop (mouse drag) and on Android (long-press) without further changes. --- .../commons/ui/components/LoadingState.kt | 33 +++++++++++++------ .../amethyst/desktop/ui/ComposeNoteDialog.kt | 25 ++++++++------ .../amethyst/desktop/ui/auth/KeyInputField.kt | 7 +++- .../amethyst/desktop/ui/auth/LoginCard.kt | 13 +++++--- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt index f3e6324f0..61d08af46 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/LoadingState.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme @@ -66,6 +67,11 @@ fun LoadingState( /** * A centered empty state with title, optional description, and optional refresh action. + * + * The optional `description` is wrapped in a [SelectionContainer] so users can + * select and copy it. `EmptyState` is reused as the in-feed error renderer + * (e.g. "Error loading feed" with the underlying error in `description`), so + * making the description selectable lets users copy error text for reporting. */ @Composable fun EmptyState( @@ -89,11 +95,13 @@ fun EmptyState( ) if (description != null) { Spacer(Modifier.height(8.dp)) - Text( - description, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - ) + SelectionContainer { + Text( + description, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + } } if (onRefresh != null) { Spacer(Modifier.height(16.dp)) @@ -106,6 +114,9 @@ fun EmptyState( /** * A centered error state with message and optional retry action. + * + * The error `message` is wrapped in a [SelectionContainer] so users can select + * and copy it — useful for reporting bugs or pasting error text into a search. */ @Composable fun ErrorState( @@ -121,11 +132,13 @@ fun ErrorState( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { - Text( - message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - ) + SelectionContainer { + Text( + message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + ) + } if (onRetry != null) { Spacer(Modifier.height(16.dp)) Button(onClick = onRetry) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index 96a804554..50b171558 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -31,6 +31,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme @@ -251,20 +252,24 @@ fun ComposeNoteDialog( errorMessage?.let { error -> Spacer(Modifier.height(8.dp)) - Text( - error, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, - ) + SelectionContainer { + Text( + error, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } } uploadState.error?.let { error -> Spacer(Modifier.height(4.dp)) - Text( - "Upload error: $error", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, - ) + SelectionContainer { + Text( + "Upload error: $error", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } } Spacer(Modifier.height(8.dp)) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt index e8f1e53b6..7fb7721a3 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop.ui.auth import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.IconButton @@ -85,7 +86,11 @@ fun KeyInputField( isError = errorMessage != null, supportingText = errorMessage?.let { - { Text(it, color = MaterialTheme.colorScheme.error) } + { + SelectionContainer { + Text(it, color = MaterialTheme.colorScheme.error) + } + } }, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt index 9ce7b7167..d6542a91d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -254,11 +255,13 @@ private fun NostrConnectContent( val clipboardManager = LocalClipboard.current if (errorMessage != null) { - Text( - errorMessage!!, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, - ) + SelectionContainer { + Text( + errorMessage!!, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + } Spacer(Modifier.height(12.dp)) Button(onClick = { errorMessage = null