feat(live-chat): display live stream raids (kind 1312) inline

Add a Quartz LiveActivitiesRaidEvent for zap.stream's kind 1312 raid
convention with root/mention a-tag markers. Wire it into the event
factory, subscribe to it in the live-activity chat filter, and route
received raids into both source and target LiveActivitiesChannel caches
so viewers on either side see the notification. Render raids as an
accent-colored pill showing "X is raiding Y" that navigates to the
target stream when tapped.

https://claude.ai/code/session_01WJA5PvDABegBYUu6h42YZi
This commit is contained in:
Claude
2026-04-20 21:29:50 +00:00
parent 238c9ea626
commit 4786647762
7 changed files with 300 additions and 2 deletions
@@ -172,6 +172,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
@@ -1495,6 +1496,26 @@ object LocalCache : ILocalCache, ICacheProvider {
return new return new
} }
fun consume(
event: LiveActivitiesRaidEvent,
relay: NormalizedRelayUrl?,
wasVerified: Boolean,
): Boolean {
val fromAddress = event.fromAddress()
val toAddress = event.toAddress()
if (fromAddress == null && toAddress == null) return false
val new = consumeRegularEvent(event, relay, wasVerified)
if (new) {
val note = getOrCreateNote(event.id)
fromAddress?.let { getOrCreateLiveChannel(it).addNote(note, relay) }
toAddress?.let { getOrCreateLiveChannel(it).addNote(note, relay) }
}
return new
}
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun consume( fun consume(
event: ChannelHideMessageEvent, event: ChannelHideMessageEvent,
@@ -2667,6 +2688,7 @@ object LocalCache : ILocalCache, ICacheProvider {
is LabeledBookmarkListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is LabeledBookmarkListEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is LiveActivitiesEvent -> consume(event, relay, wasVerified) is LiveActivitiesEvent -> consume(event, relay, wasVerified)
is LiveActivitiesChatMessageEvent -> consume(event, relay, wasVerified) is LiveActivitiesChatMessageEvent -> consume(event, relay, wasVerified)
is LiveActivitiesRaidEvent -> consume(event, relay, wasVerified)
is MeetingSpaceEvent -> consumeBaseReplaceable(event, relay, wasVerified) is MeetingSpaceEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is MeetingRoomEvent -> consumeBaseReplaceable(event, relay, wasVerified) is MeetingRoomEvent -> consumeBaseReplaceable(event, relay, wasVerified)
is MeetingRoomPresenceEvent -> consumeBaseReplaceable(event, relay, wasVerified) is MeetingRoomPresenceEvent -> consumeBaseReplaceable(event, relay, wasVerified)
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts.ChatBubbleLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChangeChannelMetadataNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChangeChannelMetadataNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatRaid
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatZap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderChatZap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderCreateChannelNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderCreateChannelNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types.RenderDraftEvent
@@ -85,6 +86,7 @@ import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEven
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.hasZapSplitSetup
import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount
@@ -111,8 +113,11 @@ fun ChatroomMessageCompose(
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav,
) { canPreview -> ) { canPreview ->
if (baseNote.event is LnZapEvent) { val event = baseNote.event
if (event is LnZapEvent) {
RenderChatZap(baseNote, accountViewModel, nav) RenderChatZap(baseNote, accountViewModel, nav)
} else if (event is LiveActivitiesRaidEvent) {
RenderChatRaid(baseNote, accountViewModel, nav)
} else { } else {
NormalChatNote( NormalChatNote(
baseNote, baseNote,
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowForwardIos
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.note.CrossfadeToDisplayComment
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent
@Composable
fun RenderChatRaid(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: INav,
) {
val raid = baseNote.event as? LiveActivitiesRaidEvent ?: return
val from = remember(raid) { raid.fromAddress() }
val to = remember(raid) { raid.toAddress() }
// Without both endpoints we can't render a meaningful raid card.
if (from == null || to == null) return
val accent = MaterialTheme.colorScheme.primary
val containerColor = remember(accent) { accent.copy(alpha = 0.14f) }
val backgroundColor = remember(containerColor) { mutableStateOf(containerColor) }
Row(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp)
.clip(RoundedCornerShape(8.dp))
.background(containerColor)
.clickable { nav.nav(to.toRoute()) }
.padding(horizontal = 10.dp, vertical = 8.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
UserPicture(from.pubKeyHex, Size20dp, Modifier, accountViewModel, nav)
Spacer(StdHorzSpacer)
LoadUser(baseUserHex = from.pubKeyHex, accountViewModel = accountViewModel) { user ->
if (user != null) {
UsernameDisplay(
baseUser = user,
fontWeight = FontWeight.Bold,
accountViewModel = accountViewModel,
)
}
}
Spacer(StdHorzSpacer)
Text(
text = stringRes(R.string.chat_raid_is_raiding),
color = accent,
fontWeight = FontWeight.Bold,
)
Spacer(StdHorzSpacer)
UserPicture(to.pubKeyHex, Size20dp, Modifier, accountViewModel, nav)
Spacer(StdHorzSpacer)
LoadUser(baseUserHex = to.pubKeyHex, accountViewModel = accountViewModel) { user ->
if (user != null) {
UsernameDisplay(
baseUser = user,
fontWeight = FontWeight.Bold,
accountViewModel = accountViewModel,
)
}
}
}
val message = raid.content
if (message.isNotBlank()) {
Spacer(Modifier.padding(top = 2.dp))
CrossfadeToDisplayComment(
comment = message,
backgroundColor = backgroundColor,
nav = nav,
accountViewModel = accountViewModel,
)
}
}
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowForwardIos,
contentDescription = null,
tint = accent,
modifier = Size20Modifier,
)
}
}
private fun Address.toRoute(): Route = Route.LiveActivityChannel(kind, pubKeyHex, dTag)
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
fun filterMessagesToLiveActivities( fun filterMessagesToLiveActivities(
@@ -36,7 +37,12 @@ fun filterMessagesToLiveActivities(
relay = it, relay = it,
filter = filter =
Filter( Filter(
kinds = listOf(LiveActivitiesChatMessageEvent.KIND, LnZapEvent.KIND), kinds =
listOf(
LiveActivitiesChatMessageEvent.KIND,
LiveActivitiesRaidEvent.KIND,
LnZapEvent.KIND,
),
tags = mapOf("a" to listOfNotNull(channel.address.toValue())), tags = mapOf("a" to listOfNotNull(channel.address.toValue())),
limit = 200, limit = 200,
since = since?.get(it)?.time, since = since?.get(it)?.time,
+1
View File
@@ -55,6 +55,7 @@
<string name="no_zap_amount_setup_long_press_to_change">No Zap Amount Setup. Long Press to change</string> <string name="no_zap_amount_setup_long_press_to_change">No Zap Amount Setup. Long Press to change</string>
<string name="chat_zap_amount_suffix">zapped %1$s sats</string> <string name="chat_zap_amount_suffix">zapped %1$s sats</string>
<string name="chat_zap_anonymous">Anonymous</string> <string name="chat_zap_anonymous">Anonymous</string>
<string name="chat_raid_is_raiding">is raiding</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">You are using a public key and public keys are read-only. Login with a Private key to be able to send zaps</string> <string name="login_with_a_private_key_to_be_able_to_send_zaps">You are using a public key and public keys are read-only. Login with a Private key to be able to send zaps</string>
<string name="login_with_a_private_key_to_be_able_to_follow">You are using a public key and public keys are read-only. Login with a Private key to be able to follow</string> <string name="login_with_a_private_key_to_be_able_to_follow">You are using a public key and public keys are read-only. Login with a Private key to be able to follow</string>
<string name="login_with_a_private_key_to_be_able_to_unfollow">You are using a public key and public keys are read-only. Login with a Private key to be able to unfollow</string> <string name="login_with_a_private_key_to_be_able_to_unfollow">You are using a public key and public keys are read-only. Login with a Private key to be able to unfollow</string>
@@ -0,0 +1,115 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip53LiveActivities.raid
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.TimeUtils
/**
* NIP-53 live stream raid (zap.stream convention, kind 1312).
*
* A raid is authored by the source streamer to redirect viewers to another live
* stream. The event carries two `a` tags referencing NIP-53 Live Activities
* (kind 30311) differentiated by NIP-10-style markers at position 3:
* - "root" -> the source stream (the raid is being sent FROM)
* - "mention" -> the target stream (the raid is being sent TO)
*
* The `content` is a free-text raid message from the source streamer.
*/
@Immutable
class LiveActivitiesRaidEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
AddressHintProvider {
override fun addressHints(): List<AddressHint> = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
fun fromActivity(): ATag? = findActivity(MARKER_ROOT)
fun toActivity(): ATag? = findActivity(MARKER_MENTION)
fun fromAddress(): Address? = fromActivity()?.let { Address(it.kind, it.pubKeyHex, it.dTag) }
fun toAddress(): Address? = toActivity()?.let { Address(it.kind, it.pubKeyHex, it.dTag) }
private fun findActivity(marker: String): ATag? =
tags
.asSequence()
.filter { it.size > 3 && it[0] == ATag.TAG_NAME && it[3] == marker }
.mapNotNull(ATag::parse)
.firstOrNull { it.kind == LiveActivitiesEvent.KIND }
companion object {
const val KIND = 1312
const val ALT = "Live activity raid"
const val MARKER_ROOT = "root"
const val MARKER_MENTION = "mention"
/**
* Builds an event template for a raid. Sender must be the host of the `from` stream.
*
* @param from source stream (the one currently live that is being ended/redirected)
* @param to target stream (where viewers should be redirected)
* @param message raid announcement text
*/
fun build(
from: EventHintBundle<LiveActivitiesEvent>,
to: EventHintBundle<LiveActivitiesEvent>,
message: String = "",
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<LiveActivitiesRaidEvent>.() -> Unit = {},
) = eventTemplate(KIND, message, createdAt) {
addMarkedATag(from, MARKER_ROOT)
addMarkedATag(to, MARKER_MENTION)
initializer()
}
private fun TagArrayBuilder<LiveActivitiesRaidEvent>.addMarkedATag(
bundle: EventHintBundle<LiveActivitiesEvent>,
marker: String,
) {
val relayUrl = bundle.relay?.url.orEmpty()
val addressId =
Address.assemble(
LiveActivitiesEvent.KIND,
bundle.event.pubKey,
bundle.event.dTag(),
)
add(arrayOf(ATag.TAG_NAME, addressId, relayUrl, marker))
}
}
}
@@ -168,6 +168,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessa
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingSpaceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
import com.vitorpamplona.quartz.nip53LiveActivities.raid.LiveActivitiesRaidEvent
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent
import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent
@@ -437,6 +438,7 @@ class EventFactory {
LabeledBookmarkListEvent.KIND -> LabeledBookmarkListEvent(id, pubKey, createdAt, tags, content, sig) LabeledBookmarkListEvent.KIND -> LabeledBookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig) LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesRaidEvent.KIND -> LiveActivitiesRaidEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)