diff --git a/amethyst/plans/2026-07-16-nip46-signer-device-checklist.md b/amethyst/plans/2026-07-16-nip46-signer-device-checklist.md index e02ca5aeab..62c14ab439 100644 --- a/amethyst/plans/2026-07-16-nip46-signer-device-checklist.md +++ b/amethyst/plans/2026-07-16-nip46-signer-device-checklist.md @@ -22,6 +22,19 @@ the client. - [ ] **Deep link**: tap a `nostrconnect://` link (web/other app) → Amethyst opens the signer screen and pairs (cold start AND already-running). +## Note preview in the sign dialog (device-only) +- [ ] A `sign_event`/publish request renders the unsigned event as a **NoteCompose + preview** (text + media + mentions, authored by the signing account), with + the "Show event" JSON toggle still available below it. +- [ ] Works for both a NIP-46 remote app and a napplet Publish/SignEvent. +- [ ] When the main Activity is gone (app fully backgrounded, only the signer + foreground service alive → `CallSessionBridge.accountViewModel` is null), + the dialog falls back to the plain content quote + JSON without crashing. +- [ ] **Risk to watch:** NoteCompose is feed UI rendered inside a standalone + dialog Activity; if it reads a CompositionLocal only provided by the main + scaffold it could crash at runtime (compiles fine). Verify on device; if it + misbehaves, the JSON fallback path is one boolean away. + ## Consent (Tier 1) - [ ] **First-connect trust picker**: a bunker-flow connect with a valid secret shows the trust-level dialog (Full trust / Reasonable / Paranoid) BEFORE any diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt index 965a90f1cd..549bf0ec03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentActivity.kt @@ -66,7 +66,13 @@ import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.call.CallSessionBridge +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.theme.AmethystTheme +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler import com.vitorpamplona.quartz.utils.TimeUtils class SignerConsentActivity : ComponentActivity() { @@ -119,6 +125,25 @@ private fun SignerConsentDialog( val scrollState = rememberScrollState() val maxHeight = LocalConfiguration.current.screenHeightDp.dp * 0.85f + // Reuse the live AccountViewModel (via CallSessionBridge, the same handle CallActivity uses) to + // render the unsigned event as a real note preview — what it will actually look like. Best-effort: + // if the main Activity is gone (only the foreground signer service alive) we fall back to the JSON. + val accountViewModel = remember { CallSessionBridge.accountViewModel } + val previewNav = remember { EmptyNav() } + val previewNote = + remember(info, accountViewModel) { + val template = info.previewTemplate + val author = info.accountPubKey ?: accountViewModel?.account?.signer?.pubKey + if (template != null && author != null && accountViewModel != null) { + runCatching { + val unsigned = RumorAssembler.assembleRumor(author, template) + accountViewModel.createTempDraftNote(unsigned, LocalCache.getOrCreateUser(author)) + }.getOrNull() + } else { + null + } + } + Dialog( onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false), @@ -173,7 +198,7 @@ private fun SignerConsentDialog( } } - val hasContent = info.contentPreview.isNotBlank() || info.rawData.isNotBlank() + val hasContent = previewNote != null || info.contentPreview.isNotBlank() || info.rawData.isNotBlank() if (hasContent) { Spacer(Modifier.height(12.dp)) Surface( @@ -185,7 +210,16 @@ private fun SignerConsentDialog( shape = MaterialTheme.shapes.medium, ) { Column(modifier = Modifier.padding(12.dp)) { - if (info.contentPreview.isNotBlank()) { + if (previewNote != null && accountViewModel != null) { + // The event rendered as it will look once signed. + NoteCompose( + baseNote = previewNote, + isQuotedNote = true, + quotesLeft = 0, + accountViewModel = accountViewModel, + nav = previewNav, + ) + } else if (info.contentPreview.isNotBlank()) { Text( "“${info.contentPreview}”", style = MaterialTheme.typography.bodySmall, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentCoordinator.kt index eb567170d8..364754f997 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/connectedApps/consent/SignerConsentCoordinator.kt @@ -24,6 +24,8 @@ import android.content.Context import android.content.Intent import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerOp import com.vitorpamplona.amethyst.commons.connectedApps.signers.SignerOpGrant +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import kotlinx.coroutines.CompletableDeferred import java.util.UUID import java.util.concurrent.ConcurrentHashMap @@ -50,6 +52,12 @@ data class SignerConsentInfo( val accountName: String? = null, val accountPicture: String? = null, val accountPubKey: String? = null, + /** + * The unsigned event a `sign_event`/publish request would sign, so the dialog can render it as a + * note preview (what it will look like) in addition to the raw JSON. Null for encrypt/decrypt and + * non-event ops. + */ + val previewTemplate: EventTemplate? = null, ) /** diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt index 455bfda6e7..cb6344be8b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip46Signer/Nip46ConsentBridge.kt @@ -118,6 +118,7 @@ object Nip46ConsentBridge { accountName = face.name, accountPicture = face.picture, accountPubKey = face.pubKey, + previewTemplate = (request as? BunkerRequestSign)?.event, ) // Fail closed if the prompt is never answered so a stuck dialog can't hold the signer hostage. return withTimeoutOrNull(CONSENT_TIMEOUT_MS) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt index fca2cb6f21..7d6932b6f6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NostrSignerOpLabels.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.connectedApps.consent.SignerConnectInfo import com.vitorpamplona.amethyst.connectedApps.consent.SignerConsentInfo import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kindNameFor +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.utils.TimeUtils @@ -86,6 +87,12 @@ fun buildSignerConsentInfo( } else -> "" } + val previewTemplate = + when (request) { + is NappletRequest.Publish -> EventTemplate(TimeUtils.now(), request.kind, request.tags, request.content) + is NappletRequest.SignEvent -> EventTemplate(request.createdAt, request.kind, request.tags, request.content) + else -> null + } return SignerConsentInfo( appletTitle = title, coordinate = identity.coordinate, @@ -94,6 +101,7 @@ fun buildSignerConsentInfo( contentPreview = preview, rawData = rawData, iconUrl = iconUrl, + previewTemplate = previewTemplate, ) }