mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 16:33:27 +00:00
feat(nip46): preview the event as a signed note in the sign dialog
The per-op consent dialog now renders a sign_event/publish request as a real NoteCompose preview — what the note will actually look like once signed (text + media + mentions, authored by the signing account) — instead of only a quoted content snippet. The "Show event" JSON toggle stays as a fallback for anyone who wants the raw payload. Reuses the live AccountViewModel via CallSessionBridge (the same handle CallActivity uses to render app UI from a standalone Activity), builds a transient unsigned Note with RumorAssembler.assembleRumor + createTempDraftNote (never persisted/verified — the same path the composer uses to preview an unsent post), and passes EmptyNav so taps don't navigate out. SignerConsentInfo carries the EventTemplate; both the NIP-46 bridge and the napplet buildSignerConsentInfo populate it for Publish/SignEvent. Falls back to the content quote + JSON when the AccountViewModel isn't available (main Activity gone) or the op has no event (encrypt/decrypt). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FHr2mu5SiHwYNR7evYUuF
This commit is contained in:
@@ -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
|
||||
|
||||
+36
-2
@@ -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<Event>(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,
|
||||
|
||||
+8
@@ -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<Event>? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -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) {
|
||||
|
||||
@@ -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<Event>(TimeUtils.now(), request.kind, request.tags, request.content)
|
||||
is NappletRequest.SignEvent -> EventTemplate<Event>(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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user