Merge #438a7e60: Add Compose previews for the multi-event approval scre…

Add Compose previews for the multi-event approval screens

nostr:nevent1qqsy8zn7vrj2slsmmx92pp4naumg6lq3z4gp52g4un2fcug3cyzu6gcpz3mhxue69uhhyetvv9ujumn8d96zuer9wcjughul

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

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
This commit is contained in:
greenart7c3
2026-07-29 11:42:19 -03:00
4 changed files with 191 additions and 24 deletions
@@ -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)
}
}
@@ -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 = {},
)
}
}
@@ -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
@@ -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) }