From 7180632a19e7ea5542cc6c96b78a679bdb94c082 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Tue, 28 Jul 2026 16:33:09 -0300 Subject: [PATCH] Add Compose previews for the multi-event approval screens Add light/dark previews for BunkerMultiEventHomeScreen and IntentMultiEventHomeScreen with sample request groups (connect, sign event, NIP-44 decrypt) so group headers, options and cards all render. Guard side effects that reach the Amber singleton, which does not exist in the preview renderer: - ProfileSubscriptionEffect: skip the relay subscription in inspection mode (also fixes every preview embedding SigningAs) - BunkerMultiEventHomeScreen: skip the encrypted-storage/DB lookup - rememberAppDisplayInfo: fall back to the package name when any part of the PackageManager lookup fails instead of crashing --- .../ui/components/AmberTopAppBar.kt | 11 +- .../components/BunkerMultiEventHomeScreen.kt | 128 +++++++++++++++--- .../components/IntentMultiEventHomeScreen.kt | 73 ++++++++++ .../components/ProfileSubscriptionEffect.kt | 3 + 4 files changed, 191 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt index 379d84c9..d0cf9b34 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/AmberTopAppBar.kt @@ -238,18 +238,13 @@ fun AmberTopAppBar( fun rememberAppDisplayInfo(packageName: String): AppDisplayInfo { val context = LocalContext.current return remember(packageName) { - val appInfo = runCatching { - context.packageManager.getApplicationInfo(packageName, 0) - }.getOrNull() - - if (appInfo != null) { + runCatching { + val appInfo = context.packageManager.getApplicationInfo(packageName, 0) AppDisplayInfo( name = context.packageManager.getApplicationLabel(appInfo).toString(), icon = context.packageManager.getApplicationIcon(appInfo), ) - } else { - AppDisplayInfo(name = packageName, icon = null) - } + }.getOrNull() ?: AppDisplayInfo(name = packageName, icon = null) } } diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt index 7b984686..b413cafd 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/BunkerMultiEventHomeScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -23,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale @@ -36,7 +38,9 @@ import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions import com.greenart7c3.nostrsigner.database.HistoryEntity import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.AmberBunkerRequest +import com.greenart7c3.nostrsigner.models.BunkerClientMetadata import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind +import com.greenart7c3.nostrsigner.models.EncryptionType import com.greenart7c3.nostrsigner.models.EventEncryptedDataKind import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind @@ -51,13 +55,21 @@ import com.greenart7c3.nostrsigner.service.RelayUrlUtils import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.service.toShortenHex import com.greenart7c3.nostrsigner.ui.RememberType +import com.greenart7c3.nostrsigner.ui.theme.AmberPreview +import com.greenart7c3.nostrsigner.ui.theme.ThemePreviews +import com.greenart7c3.nostrsigner.ui.theme.previewAccount +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.utils.TimeUtils import kotlin.collections.forEach import kotlin.collections.set import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -85,25 +97,29 @@ fun BunkerMultiEventHomeScreen( bunkerRequests.forEach { MultiEventScreenIntents.checkedStates[it.request.id] = true } } - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - localAccount = LocalPreferences.loadFromEncryptedStorage( - context, - bunkerRequests.first().currentAccount, - )?.npub?.toShortenHex() ?: "" + // Skipped in previews: encrypted storage and the Amber singleton don't + // exist in the preview renderer. + if (!LocalInspectionMode.current) { + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + localAccount = LocalPreferences.loadFromEncryptedStorage( + context, + bunkerRequests.first().currentAccount, + )?.npub?.toShortenHex() ?: "" - val app = Amber.instance.getDatabase(accountParam.npub).dao().getByKey(key) - if (ApplicationNameCache["$localAccount-$key"] == null) { - app?.let { - appName = it.application.name - ApplicationNameCache["$localAccount-$key"] = it.application.name - } - } else { - ApplicationNameCache["$localAccount-$key"]?.let { - appName = it + val app = Amber.instance.getDatabase(accountParam.npub).dao().getByKey(key) + if (ApplicationNameCache["$localAccount-$key"] == null) { + app?.let { + appName = it.application.name + ApplicationNameCache["$localAccount-$key"] = it.application.name + } + } else { + ApplicationNameCache["$localAccount-$key"]?.let { + appName = it + } } + app?.application?.icon?.let { if (it.isNotBlank()) appIcon = it } } - app?.application?.icon?.let { if (it.isNotBlank()) appIcon = it } } } @@ -592,3 +608,83 @@ private fun finishActivity(closeApp: Boolean) { private suspend fun reconnectToRelays() { Amber.instance.checkForNewRelaysAndUpdateAllFilters() } + +private const val PREVIEW_LOCAL_KEY = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + +private fun previewBunkerRequest( + request: BunkerRequest, + signedEvent: Event? = null, + encryptedData: ClearTextEncryptedDataKind? = null, +) = AmberBunkerRequest( + request = request, + localKey = PREVIEW_LOCAL_KEY, + relays = listOf(), + currentAccount = "", + nostrConnectSecret = "", + closeApplication = false, + name = "", + signedEvent = signedEvent, + encryptedData = encryptedData, + encryptionType = EncryptionType.NIP44, + isNostrConnectUri = false, + clientMetadata = BunkerClientMetadata(name = "Amethyst", url = "https://amethyst.social"), +) + +private fun previewSignRequest(id: String, content: String) = previewBunkerRequest( + request = BunkerRequestSign( + id = id, + event = EventTemplate( + createdAt = 1735689600, + kind = 1, + tags = arrayOf(arrayOf("t", "amber")), + content = content, + ), + ), + signedEvent = Event( + id = "0".repeat(64), + pubKey = PREVIEW_LOCAL_KEY, + createdAt = 1735689600, + kind = 1, + tags = arrayOf(arrayOf("t", "amber")), + content = content, + sig = "", + ), +) + +@ThemePreviews +@Composable +fun BunkerMultiEventHomeScreenPreview() { + // The screen normally resolves the app name from the database, which is + // skipped in previews — seed the cache so the header shows a real name. + ApplicationNameCache["-$PREVIEW_LOCAL_KEY"] = "Amethyst" + + AmberPreview { + BunkerMultiEventHomeScreen( + modifier = Modifier + .fillMaxWidth() + .height(700.dp) + .padding(16.dp), + bunkerRequests = persistentListOf( + previewBunkerRequest( + request = BunkerRequestConnect( + id = "preview-connect", + remoteKey = PREVIEW_LOCAL_KEY, + ), + ), + previewSignRequest("preview-sign-1", "Hello Nostr!"), + previewSignRequest("preview-sign-2", "GM from Amber"), + previewBunkerRequest( + request = BunkerRequestNip44Decrypt( + id = "preview-nip44-decrypt", + pubKey = PREVIEW_LOCAL_KEY, + ciphertext = "encrypted-payload", + ), + encryptedData = ClearTextEncryptedDataKind("encrypted-payload", "Hello Nostr!"), + ), + ), + packageName = null, + accountParam = previewAccount(), + onLoading = {}, + ) + } +} diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt index 19480725..db9a2340 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/IntentMultiEventHomeScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -38,12 +39,14 @@ import com.greenart7c3.nostrsigner.database.ApplicationWithPermissions import com.greenart7c3.nostrsigner.database.HistoryEntity import com.greenart7c3.nostrsigner.models.Account import com.greenart7c3.nostrsigner.models.ClearTextEncryptedDataKind +import com.greenart7c3.nostrsigner.models.CompressionType import com.greenart7c3.nostrsigner.models.EventEncryptedDataKind import com.greenart7c3.nostrsigner.models.IntentData import com.greenart7c3.nostrsigner.models.IntentResultType import com.greenart7c3.nostrsigner.models.Permission import com.greenart7c3.nostrsigner.models.PrivateZapEncryptedDataKind import com.greenart7c3.nostrsigner.models.Result +import com.greenart7c3.nostrsigner.models.ReturnType import com.greenart7c3.nostrsigner.models.SignerType import com.greenart7c3.nostrsigner.models.TagArrayEncryptedDataKind import com.greenart7c3.nostrsigner.service.AmberUtils @@ -51,9 +54,14 @@ import com.greenart7c3.nostrsigner.service.MultiEventScreenIntents import com.greenart7c3.nostrsigner.service.RelayUrlUtils import com.greenart7c3.nostrsigner.service.model.AmberEvent import com.greenart7c3.nostrsigner.ui.RememberType +import com.greenart7c3.nostrsigner.ui.theme.AmberPreview +import com.greenart7c3.nostrsigner.ui.theme.ThemePreviews +import com.greenart7c3.nostrsigner.ui.theme.previewAccount +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -508,6 +516,71 @@ private fun IntentRequestCard( } } +private const val PREVIEW_PUBKEY = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + +private fun previewIntent( + id: String, + type: SignerType, + data: String = "", + event: Event? = null, + encryptedData: ClearTextEncryptedDataKind? = null, +) = IntentData( + data = data, + name = "", + type = type, + pubKey = PREVIEW_PUBKEY, + id = id, + callBackUrl = null, + compression = CompressionType.NONE, + returnType = ReturnType.SIGNATURE, + permissions = null, + currentAccount = "", + route = null, + event = event, + encryptedData = encryptedData, +) + +private fun previewSignIntent(id: String, content: String) = previewIntent( + id = id, + type = SignerType.SIGN_EVENT, + event = Event( + id = "0".repeat(64), + pubKey = PREVIEW_PUBKEY, + createdAt = 1735689600, + kind = 1, + tags = arrayOf(arrayOf("t", "amber")), + content = content, + sig = "", + ), +) + +@ThemePreviews +@Composable +fun IntentMultiEventHomeScreenPreview() { + AmberPreview { + IntentMultiEventHomeScreen( + modifier = Modifier + .fillMaxWidth() + .height(700.dp) + .padding(16.dp), + intents = persistentListOf( + previewSignIntent("preview-sign-1", "Hello Nostr!"), + previewSignIntent("preview-sign-2", "GM from Amber"), + previewIntent( + id = "preview-nip44-decrypt", + type = SignerType.NIP44_DECRYPT, + data = "encrypted-payload", + encryptedData = ClearTextEncryptedDataKind("encrypted-payload", "Hello Nostr!"), + ), + ), + packageName = "com.vitorpamplona.amethyst", + accountParam = previewAccount(), + onRemoveIntentData = { _, _ -> }, + onLoading = {}, + ) + } +} + private fun finishActivity(closeApp: Boolean) { val activity = Amber.instance.getMainActivity() activity?.intent = null diff --git a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfileSubscriptionEffect.kt b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfileSubscriptionEffect.kt index a8c39c03..6d1cd7e7 100644 --- a/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfileSubscriptionEffect.kt +++ b/app/src/main/java/com/greenart7c3/nostrsigner/ui/components/ProfileSubscriptionEffect.kt @@ -2,6 +2,7 @@ package com.greenart7c3.nostrsigner.ui.components import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalInspectionMode import com.greenart7c3.nostrsigner.Amber import com.greenart7c3.nostrsigner.models.Account import kotlinx.coroutines.launch @@ -14,6 +15,8 @@ import kotlinx.coroutines.launch */ @Composable fun ProfileSubscriptionEffect(account: Account) { + // No relay subscriptions in previews: the Amber singleton doesn't exist in the preview renderer. + if (LocalInspectionMode.current) return DisposableEffect(account.hexKey) { val sub = Amber.instance.profileSubscription Amber.instance.applicationIOScope.launch { sub.updateFilter(account) }