fix(marmot): route reply button on MLS messages to the encrypted group
Tapping reply on a Marmot/MLS (kind:445) message in the Notifications screen previously fell through routeReplyTo()'s `else` branch and opened the generic public-comment composer (Route.GenericCommentPost). Sending it would publish a plaintext kind:1111 that references the encrypted inner event id, leaking that the user has decrypted that group message. Mirror the NIP-17 pattern (Route.Room carries replyId, draftId, draftMessage; the same chat screen renders the "replying to" quote above the input) for MLS: - routeReplyTo() now detects MarmotGroupChatroom in note.inGatherers, matching how routeFor() at line 67 already finds the parent group, and returns Route.MarmotGroupChat(groupId, replyId = note.idHex). - Route.MarmotGroupChat gains message/replyId/draftId fields. - MarmotGroupChatView resolves the replyId into a Note, shows DisplayReplyingToNote above the composer, wires onWantsToReply for in-chat replies, and threads the parent inner event through AccountViewModel.sendMarmotGroupMessage into MarmotManager.buildTextMessage, which now adds a NIP-18 q-tag on the inner kind:9 (the same convention ChatEvent.reply() uses). Push notifications: notifyGroupMessage previously passed chatroomMembers=null, so the inline Reply action was never attached for MLS group notifications. Add a parallel MARMOT_REPLY_ACTION wired with the group id + parent inner event id; NotificationReplyReceiver loads the account, rebuilds the parent inner event from LocalCache (or sends unthreaded if the cache was pruned), and publishes the reply through the same MarmotManager path — so the inline notification reply stays encrypted inside the group instead of taking the NIP-17 PTag fallback.
This commit is contained in:
+2
@@ -586,6 +586,8 @@ class EventNotificationConsumer(
|
||||
accountNpub = accountNpub,
|
||||
accountPictureUrl = account.userProfile().profilePicture(),
|
||||
chatroomMembers = null,
|
||||
marmotNostrGroupId = nostrGroupId,
|
||||
marmotReplyToInnerEventId = innerEvent.id,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+63
@@ -29,7 +29,10 @@ import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
@@ -94,6 +97,24 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
||||
sendPublicReply(accountNpub, targetEventId, replyText)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationUtils.MARMOT_REPLY_ACTION -> {
|
||||
val replyText =
|
||||
RemoteInput
|
||||
.getResultsFromIntent(intent)
|
||||
?.getCharSequence(NotificationUtils.KEY_REPLY_TEXT)
|
||||
?.toString()
|
||||
|
||||
if (replyText.isNullOrBlank()) return
|
||||
|
||||
val accountNpub = intent.getStringExtra(NotificationUtils.KEY_ACCOUNT_NPUB) ?: return
|
||||
val nostrGroupId = intent.getStringExtra(NotificationUtils.KEY_MARMOT_GROUP_ID) ?: return
|
||||
val replyToInnerId = intent.getStringExtra(NotificationUtils.KEY_MARMOT_REPLY_TO_INNER_ID)
|
||||
|
||||
runOnRelay(notificationManager, notificationId) {
|
||||
sendMarmotReply(accountNpub, nostrGroupId, replyToInnerId, replyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +161,48 @@ class NotificationReplyReceiver : BroadcastReceiver() {
|
||||
account.sendNip17PrivateMessage(template)
|
||||
}
|
||||
|
||||
private suspend fun sendMarmotReply(
|
||||
accountNpub: String,
|
||||
nostrGroupId: String,
|
||||
replyToInnerEventId: String?,
|
||||
replyText: String,
|
||||
) {
|
||||
val accountSettings = LocalPreferences.loadAccountConfigFromEncryptedStorage(accountNpub) ?: return
|
||||
val account = Amethyst.instance.accountsCache.loadAccount(accountSettings)
|
||||
|
||||
val manager = account.marmotManager ?: return
|
||||
|
||||
// Recover the parent inner event so the kind:9 reply carries the
|
||||
// proper q-tag. Inner events live in LocalCache keyed by the inner
|
||||
// id; if we somehow miss it (e.g. cache was pruned) the reply still
|
||||
// goes through unthreaded — better than dropping the user's message.
|
||||
val replyToInnerEvent: Event? =
|
||||
replyToInnerEventId?.let { LocalCache.getNoteIfExists(it)?.event }
|
||||
|
||||
val bundle =
|
||||
manager.buildTextMessage(
|
||||
nostrGroupId = nostrGroupId,
|
||||
text = replyText,
|
||||
replyTo = replyToInnerEvent,
|
||||
persistOwn = false,
|
||||
)
|
||||
|
||||
// Mirror AccountViewModel.marmotGroupRelays(): prefer the group's
|
||||
// configured relays from MLS GroupContext metadata, fall back to
|
||||
// the account's outbox set so a misconfigured group doesn't silently
|
||||
// drop the reply.
|
||||
val groupRelays: Set<NormalizedRelayUrl> =
|
||||
manager
|
||||
.groupMetadata(nostrGroupId)
|
||||
?.relays
|
||||
?.mapNotNull { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
?.toSet()
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?: account.outboxRelays.flow.value
|
||||
|
||||
account.sendMarmotGroupMessage(nostrGroupId, bundle.innerEvent, groupRelays)
|
||||
}
|
||||
|
||||
private suspend fun sendPublicReply(
|
||||
accountNpub: String,
|
||||
targetEventId: String,
|
||||
|
||||
+50
@@ -60,12 +60,15 @@ object NotificationUtils {
|
||||
|
||||
const val REPLY_ACTION = "com.vitorpamplona.amethyst.REPLY_ACTION"
|
||||
const val PUBLIC_REPLY_ACTION = "com.vitorpamplona.amethyst.PUBLIC_REPLY_ACTION"
|
||||
const val MARMOT_REPLY_ACTION = "com.vitorpamplona.amethyst.MARMOT_REPLY_ACTION"
|
||||
const val MARK_READ_ACTION = "com.vitorpamplona.amethyst.MARK_READ_ACTION"
|
||||
const val KEY_REPLY_TEXT = "key_reply_text"
|
||||
const val KEY_NOTIFICATION_ID = "key_notification_id"
|
||||
const val KEY_ACCOUNT_NPUB = "key_account_npub"
|
||||
const val KEY_CHATROOM_MEMBERS = "key_chatroom_members"
|
||||
const val KEY_TARGET_EVENT_ID = "key_target_event_id"
|
||||
const val KEY_MARMOT_GROUP_ID = "key_marmot_group_id"
|
||||
const val KEY_MARMOT_REPLY_TO_INNER_ID = "key_marmot_reply_to_inner_id"
|
||||
|
||||
private const val DM_SUMMARY_ID = 0x10000
|
||||
private const val ZAP_SUMMARY_ID = 0x20000
|
||||
@@ -374,6 +377,8 @@ object NotificationUtils {
|
||||
accountNpub: String? = null,
|
||||
accountPictureUrl: String? = null,
|
||||
chatroomMembers: String? = null,
|
||||
marmotNostrGroupId: String? = null,
|
||||
marmotReplyToInnerEventId: String? = null,
|
||||
) {
|
||||
getOrCreateDMChannel(applicationContext)
|
||||
val channelId = stringRes(applicationContext, R.string.app_notification_dms_channel_id)
|
||||
@@ -390,6 +395,8 @@ object NotificationUtils {
|
||||
accountNpub = accountNpub,
|
||||
accountPictureUrl = accountPictureUrl,
|
||||
chatroomMembers = chatroomMembers,
|
||||
marmotNostrGroupId = marmotNostrGroupId,
|
||||
marmotReplyToInnerEventId = marmotReplyToInnerEventId,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -425,6 +432,8 @@ object NotificationUtils {
|
||||
accountNpub: String?,
|
||||
accountPictureUrl: String?,
|
||||
chatroomMembers: String?,
|
||||
marmotNostrGroupId: String? = null,
|
||||
marmotReplyToInnerEventId: String? = null,
|
||||
) {
|
||||
val notId = id.hashCode()
|
||||
|
||||
@@ -522,6 +531,47 @@ object NotificationUtils {
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.build()
|
||||
|
||||
builder.addAction(replyAction)
|
||||
} else if (accountNpub != null && marmotNostrGroupId != null) {
|
||||
// Marmot/MLS Reply action: sends the user's text as an encrypted
|
||||
// kind:9 inside the Marmot group, replying to the inner event
|
||||
// that triggered this notification. Mirrors the NIP-17 path
|
||||
// above but routes through NotificationReplyReceiver's
|
||||
// MARMOT_REPLY_ACTION branch so we never publish a plaintext
|
||||
// public reply for an encrypted group message.
|
||||
val remoteInput =
|
||||
RemoteInput
|
||||
.Builder(KEY_REPLY_TEXT)
|
||||
.setLabel(stringRes(applicationContext, R.string.app_notification_reply_label))
|
||||
.build()
|
||||
|
||||
val replyIntent =
|
||||
Intent(applicationContext, NotificationReplyReceiver::class.java).apply {
|
||||
action = MARMOT_REPLY_ACTION
|
||||
putExtra(KEY_NOTIFICATION_ID, notId)
|
||||
putExtra(KEY_ACCOUNT_NPUB, accountNpub)
|
||||
putExtra(KEY_MARMOT_GROUP_ID, marmotNostrGroupId)
|
||||
if (marmotReplyToInnerEventId != null) {
|
||||
putExtra(KEY_MARMOT_REPLY_TO_INNER_ID, marmotReplyToInnerEventId)
|
||||
}
|
||||
}
|
||||
|
||||
val replyPendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
applicationContext,
|
||||
notId,
|
||||
replyIntent,
|
||||
PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
)
|
||||
|
||||
val replyAction =
|
||||
NotificationCompat.Action
|
||||
.Builder(R.drawable.amethyst, stringRes(applicationContext, R.string.app_notification_reply_label), replyPendingIntent)
|
||||
.addRemoteInput(remoteInput)
|
||||
.setAllowGeneratedReplies(true)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.build()
|
||||
|
||||
builder.addAction(replyAction)
|
||||
}
|
||||
|
||||
|
||||
@@ -368,7 +368,16 @@ fun BuildNavigation(
|
||||
composableFromEndArgs<Route.RoomByAuthor> { ChatroomByAuthorScreen(it.id, null, accountViewModel, nav) }
|
||||
|
||||
composableFromEnd<Route.MarmotGroupList> { MarmotGroupListScreen(accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.MarmotGroupChat> { MarmotGroupChatScreen(it.nostrGroupId, accountViewModel, nav) }
|
||||
composableFromEndArgs<Route.MarmotGroupChat> {
|
||||
MarmotGroupChatScreen(
|
||||
nostrGroupId = it.nostrGroupId,
|
||||
draftMessage = it.message,
|
||||
replyToInnerNote = it.replyId,
|
||||
editFromDraft = it.draftId,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
composableFromEndArgs<Route.MarmotGroupInfo> { MarmotGroupInfoScreen(it.nostrGroupId, accountViewModel, nav) }
|
||||
|
||||
composableFromBottom<Route.CreateMarmotGroup> { CreateGroupScreen(accountViewModel, nav) }
|
||||
|
||||
@@ -252,6 +252,15 @@ fun routeReplyTo(
|
||||
note: Note,
|
||||
account: Account,
|
||||
): Route? {
|
||||
// Marmot group messages must reply inside the encrypted group, not as a
|
||||
// public kind:1111 comment. The inner kind:9 event has no group hint of
|
||||
// its own — we detect the group via the gathering MarmotGroupChatroom,
|
||||
// mirroring routeFor() above.
|
||||
val marmotGroup = note.inGatherers?.firstNotNullOfOrNull { it as? MarmotGroupChatroom }
|
||||
if (marmotGroup != null) {
|
||||
return Route.MarmotGroupChat(marmotGroup.nostrGroupId, replyId = note.idHex)
|
||||
}
|
||||
|
||||
val noteEvent = note.event
|
||||
return when (noteEvent) {
|
||||
is ChannelMessageEvent -> {
|
||||
|
||||
@@ -404,6 +404,9 @@ sealed class Route {
|
||||
|
||||
@Serializable data class MarmotGroupChat(
|
||||
val nostrGroupId: String,
|
||||
val message: String? = null,
|
||||
val replyId: HexKey? = null,
|
||||
val draftId: HexKey? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable data class MarmotGroupInfo(
|
||||
|
||||
+5
-1
@@ -1541,11 +1541,15 @@ class AccountViewModel(
|
||||
suspend fun sendMarmotGroupMessage(
|
||||
nostrGroupId: String,
|
||||
text: String,
|
||||
replyToInnerEvent: Event? = null,
|
||||
) {
|
||||
// Inner event construction lives on MarmotManager so CLI and UI don't drift.
|
||||
// persistOwn=false because Account.sendMarmotGroupMessage routes the outer
|
||||
// event through LocalCache which already handles own-message display.
|
||||
val bundle = account.marmotManager?.buildTextMessage(nostrGroupId, text, persistOwn = false) ?: return
|
||||
val bundle =
|
||||
account.marmotManager
|
||||
?.buildTextMessage(nostrGroupId, text, replyTo = replyToInnerEvent, persistOwn = false)
|
||||
?: return
|
||||
val relays = marmotGroupRelays(nostrGroupId)
|
||||
account.sendMarmotGroupMessage(nostrGroupId, bundle.innerEvent, relays)
|
||||
}
|
||||
|
||||
+6
@@ -51,6 +51,9 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@Composable
|
||||
fun MarmotGroupChatScreen(
|
||||
nostrGroupId: HexKey,
|
||||
draftMessage: String? = null,
|
||||
replyToInnerNote: HexKey? = null,
|
||||
editFromDraft: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -127,6 +130,9 @@ fun MarmotGroupChatScreen(
|
||||
Column(Modifier.padding(it)) {
|
||||
MarmotGroupChatView(
|
||||
nostrGroupId = nostrGroupId,
|
||||
draftMessage = draftMessage,
|
||||
replyToInnerNote = replyToInnerNote,
|
||||
editFromDraft = editFromDraft,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
|
||||
+52
-3
@@ -29,11 +29,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.clearText
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -47,6 +50,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
|
||||
import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField
|
||||
@@ -58,6 +62,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.Marm
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.marmotGroup.send.MarmotFileUploader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
@@ -74,6 +79,9 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun MarmotGroupChatView(
|
||||
nostrGroupId: HexKey,
|
||||
draftMessage: String? = null,
|
||||
replyToInnerNote: HexKey? = null,
|
||||
@Suppress("UNUSED_PARAMETER") editFromDraft: HexKey? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
@@ -99,6 +107,32 @@ fun MarmotGroupChatView(
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
val messageState = remember(nostrGroupId) { TextFieldState() }
|
||||
val replyTo = remember(nostrGroupId) { mutableStateOf<Note?>(null) }
|
||||
|
||||
// Resolve the navigation-supplied replyId (e.g. tapping reply on an MLS
|
||||
// message in the Notifications screen) into the actual Note once it has
|
||||
// landed in LocalCache. checkGetOrCreateNote is a no-op for unknown ids.
|
||||
if (replyToInnerNote != null) {
|
||||
LaunchedEffect(replyToInnerNote) {
|
||||
val parent = accountViewModel.checkGetOrCreateNote(replyToInnerNote)
|
||||
if (parent != null) {
|
||||
replyTo.value = parent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (draftMessage != null) {
|
||||
LaunchedEffect(draftMessage) {
|
||||
messageState.setTextAndPlaceCursorAtEnd(draftMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// editFromDraft is accepted for route symmetry with NIP-17's
|
||||
// Route.Room, but Marmot doesn't yet persist drafts, so it's currently
|
||||
// unused. Suppressed at the parameter rather than via a fake binding
|
||||
// so ktlint stays happy.
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier =
|
||||
@@ -111,7 +145,7 @@ fun MarmotGroupChatView(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
routeForLastRead = "MarmotGroup/$nostrGroupId",
|
||||
onWantsToReply = { },
|
||||
onWantsToReply = { note -> replyTo.value = note },
|
||||
onWantsToEditDraft = { },
|
||||
)
|
||||
}
|
||||
@@ -120,6 +154,8 @@ fun MarmotGroupChatView(
|
||||
|
||||
MarmotGroupMessageComposer(
|
||||
nostrGroupId = nostrGroupId,
|
||||
messageState = messageState,
|
||||
replyTo = replyTo,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
onMessageSent = {
|
||||
@@ -132,12 +168,13 @@ fun MarmotGroupChatView(
|
||||
@Composable
|
||||
fun MarmotGroupMessageComposer(
|
||||
nostrGroupId: HexKey,
|
||||
messageState: TextFieldState = remember { TextFieldState() },
|
||||
replyTo: MutableState<Note?> = remember { mutableStateOf(null) },
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
onMessageSent: suspend () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val messageState = remember { TextFieldState() }
|
||||
val canPost by remember { derivedStateOf { messageState.text.isNotBlank() } }
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -162,6 +199,12 @@ fun MarmotGroupMessageComposer(
|
||||
)
|
||||
}
|
||||
|
||||
replyTo.value?.let {
|
||||
DisplayReplyingToNote(it, accountViewModel, nav) {
|
||||
replyTo.value = null
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = EditFieldModifier) {
|
||||
ThinPaddingTextField(
|
||||
state = messageState,
|
||||
@@ -191,10 +234,16 @@ fun MarmotGroupMessageComposer(
|
||||
) {
|
||||
val text = messageState.text.toString().trim()
|
||||
if (text.isNotEmpty()) {
|
||||
val replyParent = replyTo.value?.event
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
accountViewModel.sendMarmotGroupMessage(nostrGroupId, text)
|
||||
accountViewModel.sendMarmotGroupMessage(
|
||||
nostrGroupId = nostrGroupId,
|
||||
text = text,
|
||||
replyToInnerEvent = replyParent,
|
||||
)
|
||||
messageState.clearText()
|
||||
replyTo.value = null
|
||||
onMessageSent()
|
||||
} catch (e: Exception) {
|
||||
launch(Dispatchers.Main) {
|
||||
|
||||
+17
-1
@@ -46,6 +46,8 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag
|
||||
import com.vitorpamplona.quartz.nip18Reposts.quotes.quote
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlin.io.encoding.Base64
|
||||
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||
@@ -182,11 +184,25 @@ class MarmotManager(
|
||||
suspend fun buildTextMessage(
|
||||
nostrGroupId: HexKey,
|
||||
text: String,
|
||||
replyTo: Event? = null,
|
||||
persistOwn: Boolean = true,
|
||||
): TextMessageBundle {
|
||||
val template =
|
||||
com.vitorpamplona.quartz.nip01Core.signers
|
||||
.eventTemplate<Event>(kind = 9, description = text)
|
||||
.eventTemplate<Event>(kind = 9, description = text) {
|
||||
if (replyTo != null) {
|
||||
// Mirror ChatEvent.reply(): NIP-18 q-tag references the
|
||||
// parent inner kind:9 by id (+ author, no relay hint —
|
||||
// the inner rumor never hits a relay directly).
|
||||
quote(
|
||||
QEventTag(
|
||||
eventId = replyTo.id,
|
||||
relayHint = null,
|
||||
authorPubKeyHex = replyTo.pubKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val innerEvent =
|
||||
com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorAssembler
|
||||
.assembleRumor<Event>(signer.pubKey, template)
|
||||
|
||||
Reference in New Issue
Block a user