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:
@@ -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)
|
||||||
|
|||||||
+6
-1
@@ -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,
|
||||||
|
|||||||
+147
@@ -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)
|
||||||
+7
-1
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+115
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user