From ef1d63177b7c3696b74870dca030389e22320a83 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 16:23:42 +0000 Subject: [PATCH] Convert crash report dialog to a full screen with editable content - Replace CrashReportAlertDialog with CrashReportScreen, a full navigation screen - Show the crash report content in an editable OutlinedTextField so users can modify before sending - Store pending crash report in Amber.instance.pendingCrashReport after loadAndDelete - DisplayCrashMessages now navigates to Route.CrashReport instead of showing a dialog - Add CrashReport route to Route.kt and register composable in MainScreen NavHost https://claude.ai/code/session_01BjzLcsaetwGo7rPeVbgmSq --- .../java/com/greenart7c3/nostrsigner/Amber.kt | 1 + .../crashreports/DisplayCrashMessages.kt | 151 +------------- .../nostrsigner/ui/CrashReportScreen.kt | 196 ++++++++++++++++++ .../greenart7c3/nostrsigner/ui/MainScreen.kt | 23 +- .../nostrsigner/ui/navigation/Route.kt | 7 + 5 files changed, 236 insertions(+), 142 deletions(-) create mode 100644 app/src/main/java/com/greenart7c3/nostrsigner/ui/CrashReportScreen.kt diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt index 64e4048c..a96d9117 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/Amber.kt @@ -81,6 +81,7 @@ class Amber : SingletonImageLoader.Factory { private var mainActivityRef: WeakReference? = null val crashReportCache: CrashReportCache by lazy { CrashReportCache(this.applicationContext) } + var pendingCrashReport: String? = null fun setMainActivity(activity: AppCompatActivity?) { Log.d(TAG, "Setting main activity ref to $activity") diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/DisplayCrashMessages.kt b/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/DisplayCrashMessages.kt index 4d98bdb6..678fc768 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/DisplayCrashMessages.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/service/crashreports/DisplayCrashMessages.kt @@ -20,160 +20,29 @@ */ package com.greenart7c3.nostrsigner.service.crashreports -import android.content.ClipData -import android.content.Intent -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Done -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ClipEntry -import androidx.compose.ui.platform.LocalClipboard -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri +import androidx.navigation.NavHostController import com.greenart7c3.nostrsigner.Amber -import com.greenart7c3.nostrsigner.BuildFlavorChecker -import com.greenart7c3.nostrsigner.R import com.greenart7c3.nostrsigner.models.Account -import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.tags.people.PTag -import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent -import com.vitorpamplona.quartz.nip19Bech32.toNpub -import com.vitorpamplona.quartz.nip40Expiration.expiration -import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.TimeUtils +import com.greenart7c3.nostrsigner.ui.navigation.Route import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable fun DisplayCrashMessages( account: Account, + navController: NavHostController, ) { - val stackTrace = remember { mutableStateOf(null) } - val clipboardManager = LocalClipboard.current - LaunchedEffect(account) { withContext(Dispatchers.IO) { - stackTrace.value = Amber.instance.crashReportCache.loadAndDelete() - } - } - - stackTrace.value?.let { stack -> - if (BuildFlavorChecker.isOfflineFlavor()) { - CrashReportAlertDialog( - text = stringResource(R.string.copy_crash_report_to_clipboard), - onDismiss = { stackTrace.value = null }, - onConfirm = { - Amber.instance.applicationIOScope.launch(Dispatchers.Main) { - try { - clipboardManager.setClipEntry( - ClipEntry( - ClipData.newPlainText("", stack), - ), - ) - val intent = Intent(Intent.ACTION_VIEW) - val npub = Hex.decode("7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19").toNpub() - intent.data = "nostr:$npub".toUri() - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - Amber.instance.startActivity(intent) - stackTrace.value = null - } catch (_: Exception) { - stackTrace.value = null - } - } - }, - ) - } else { - CrashReportAlertDialog( - text = stringResource(R.string.would_you_like_to_send_the_recent_crash_report_to_amber_in_a_dm_no_personal_information_will_be_shared), - onDismiss = { stackTrace.value = null }, - onConfirm = { - Amber.instance.applicationIOScope.launch { - val client = NostrClient(Amber.instance.factory, Amber.instance.applicationIOScope) - client.connect() - - val template = ChatMessageEvent.build( - msg = stack, - to = listOf(PTag("7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19")), - createdAt = System.currentTimeMillis() / 1000, - ) { - val thirtyDaysInSeconds = 30L * 86_400 - expiration(TimeUtils.now() + thirtyDaysInSeconds) - } - val signedEvents = account.createMessageNIP17(template) - signedEvents.wraps.forEach { wrap -> - client.send( - event = wrap, - relayList = setOf( - NormalizedRelayUrl(url = "wss://inbox.nostr.wine"), - NormalizedRelayUrl(url = "wss://nostr.land"), - ), - ) - } - stackTrace.value = null - delay(10000) - client.disconnect() - } - }, - ) - } - } -} - -@Composable -fun CrashReportAlertDialog( - onDismiss: () -> Unit, - onConfirm: () -> Unit, - text: String, -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.crashreport_found)) }, - text = { - SelectionContainer { - Text(text) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - Button( - onClick = onConfirm, - contentPadding = PaddingValues(horizontal = 16.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = Icons.Outlined.Done, - tint = Color.Black, - contentDescription = stringResource(R.string.crashreport_found_send), - ) - Spacer(Modifier.size(4.dp)) - Text(stringResource(R.string.crashreport_found_send), color = Color.Black) + val stack = Amber.instance.crashReportCache.loadAndDelete() + if (stack != null) { + Amber.instance.pendingCrashReport = stack + withContext(Dispatchers.Main) { + navController.navigate(Route.CrashReport.route) } } - }, - ) + } + } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/CrashReportScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/CrashReportScreen.kt new file mode 100644 index 00000000..a2af2f93 --- /dev/null +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/CrashReportScreen.kt @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.greenart7c3.nostrsigner.ui + +import android.content.ClipData +import android.content.Intent +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Done +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ClipEntry +import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import com.greenart7c3.nostrsigner.Amber +import com.greenart7c3.nostrsigner.BuildFlavorChecker +import com.greenart7c3.nostrsigner.R +import com.greenart7c3.nostrsigner.models.Account +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip19Bech32.toNpub +import com.vitorpamplona.quartz.nip40Expiration.expiration +import com.vitorpamplona.quartz.utils.Hex +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun CrashReportScreen( + modifier: Modifier = Modifier, + account: Account, + onDismiss: () -> Unit, +) { + val initialStack = remember { Amber.instance.pendingCrashReport ?: "" } + var editedStack by remember { mutableStateOf(initialStack) } + val clipboardManager = LocalClipboard.current + + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = if (BuildFlavorChecker.isOfflineFlavor()) { + stringResource(R.string.copy_crash_report_to_clipboard) + } else { + stringResource(R.string.would_you_like_to_send_the_recent_crash_report_to_amber_in_a_dm_no_personal_information_will_be_shared) + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + OutlinedTextField( + value = editedStack, + onValueChange = { editedStack = it }, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedButton( + modifier = Modifier.weight(1f), + onClick = { + Amber.instance.pendingCrashReport = null + onDismiss() + }, + ) { + Text(stringResource(R.string.cancel)) + } + + Button( + modifier = Modifier.weight(1f), + onClick = { + val report = editedStack.ifBlank { return@Button } + if (BuildFlavorChecker.isOfflineFlavor()) { + Amber.instance.applicationIOScope.launch(Dispatchers.Main) { + try { + clipboardManager.setClipEntry( + ClipEntry(ClipData.newPlainText("", report)), + ) + val intent = Intent(Intent.ACTION_VIEW) + val npub = Hex.decode("7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19").toNpub() + intent.data = "nostr:$npub".toUri() + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + Amber.instance.startActivity(intent) + Amber.instance.pendingCrashReport = null + onDismiss() + } catch (_: Exception) { + Amber.instance.pendingCrashReport = null + onDismiss() + } + } + } else { + Amber.instance.applicationIOScope.launch { + val client = NostrClient(Amber.instance.factory, Amber.instance.applicationIOScope) + client.connect() + + val template = ChatMessageEvent.build( + msg = report, + to = listOf(PTag("7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19")), + createdAt = System.currentTimeMillis() / 1000, + ) { + val thirtyDaysInSeconds = 30L * 86_400 + expiration(TimeUtils.now() + thirtyDaysInSeconds) + } + val signedEvents = account.createMessageNIP17(template) + signedEvents.wraps.forEach { wrap -> + client.send( + event = wrap, + relayList = setOf( + NormalizedRelayUrl(url = "wss://inbox.nostr.wine"), + NormalizedRelayUrl(url = "wss://nostr.land"), + ), + ) + } + Amber.instance.pendingCrashReport = null + onDismiss() + delay(10000) + client.disconnect() + } + } + }, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Done, + tint = Color.Black, + contentDescription = stringResource(R.string.crashreport_found_send), + ) + Spacer(Modifier.size(4.dp)) + Text(stringResource(R.string.crashreport_found_send), color = Color.Black) + } + } + } + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt index a2bad640..9c040705 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/MainScreen.kt @@ -66,6 +66,7 @@ import com.greenart7c3.nostrsigner.models.TorMode import com.greenart7c3.nostrsigner.okhttp.HttpClientManager import com.greenart7c3.nostrsigner.service.TorManager import com.greenart7c3.nostrsigner.service.crashreports.DisplayCrashMessages +import com.greenart7c3.nostrsigner.ui.CrashReportScreen import com.greenart7c3.nostrsigner.ui.actions.AccountBackupScreen import com.greenart7c3.nostrsigner.ui.actions.AccountsBottomSheet import com.greenart7c3.nostrsigner.ui.actions.ActiveRelaysScreen @@ -936,9 +937,29 @@ fun MainScreen( ) }, ) + + composable( + Route.CrashReport.route, + content = { + CrashReportScreen( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .consumeWindowInsets(padding) + .padding(horizontal = verticalPadding) + .padding(top = verticalPadding * 1.5f), + account = account, + onDismiss = { + Amber.instance.applicationIOScope.launch(Dispatchers.Main) { + navController.navigateUp() + } + }, + ) + }, + ) } } - DisplayCrashMessages(account) + DisplayCrashMessages(account, navController) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt index dc5cae63..efae3644 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/navigation/Route.kt @@ -190,6 +190,12 @@ sealed class Route( route = "Activities", icon = R.drawable.incoming_request, ) + + data object CrashReport : Route( + title = Amber.instance.getString(R.string.crashreport_found), + route = "CrashReport", + icon = R.drawable.settings, + ) } val routes = listOf( @@ -221,4 +227,5 @@ val routes = listOf( Route.EditProfile, Route.Feedback, Route.Activities, + Route.CrashReport, )