From ef8f49733a2fd76e11d41245eac711e134ba33a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 22:35:15 +0000 Subject: [PATCH] feat(nutzap): notifications card surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the nutzap UX integration. Inbound NIP-61 nutzaps (kind 9321) now appear in the notifications feed alongside lightning zaps, boosts, likes, and onchain zaps — but with the cashu icon so the rail is visible at a glance. Pieces: - NotificationFeedFilter.NOTIFICATION_KINDS now includes NutzapEvent.KIND. The existing tagsAnEventByUser logic falls through to its `return true` default for nutzap (it's not a BaseNoteEvent / ReactionEvent / Repost / Git / Highlight), and the kind:9321's `p` tag carries the recipient so isTaggedUser matches. - CardFeedContentState gains a parallel nutzap grouping pass. Nutzaps targeting a specific note feed into nutzapsPerEvent and roll into the per-note MultiSetCard; nutzaps without an e-tag target (recipient-only) feed into nutzapsPerUser and surface as a NutzapUserSetCard per sender per day. - MultiSetCard extended with `nutzapEvents: ImmutableList`, and its min/max createdAt cover the cashu rail too. Adding a new field at the end with a `persistentListOf()` default keeps the existing constructor sites compatible. - New NutzapUserSetCard for the per-sender aggregate. Wraps raw kind:9321 Notes (no request/response pair like LN), keyed by pubkey+createdAt with an "N" suffix so it never collides with the LN ZapUserSetCard. - MultiSetCompose: new RenderNutzapGallery row renders the cashu icon (CustomHashTagIcons.Cashu, tint=Unspecified to preserve the brand colour) followed by the same AuthorGalleryZaps the lightning rail uses. Each kind:9321 is mapped to a ZapAmountCommentNotification with the claimed sat total and the event content as the comment. - New NutzapUserSetCompose modelled on ZapUserSetCompose, also wired into CardFeedView's card switch. The user-facing result: the notifications screen now shows a "X sent you Y sats via cashu" card with the cashu icon, exactly the same shape as the lightning version, and the per-note multi-card grows a cashu rail when the note has cashu zaps. Phase 4 next: the dedicated cashu line in ReactionDetailGallery, modelled on the existing onchain row. --- .../amethyst/ui/note/MultiSetCompose.kt | 57 +++++++ .../amethyst/ui/note/NutzapUserSetCompose.kt | 153 ++++++++++++++++++ .../notifications/CardFeedContentState.kt | 63 +++++++- .../loggedIn/notifications/CardFeedState.kt | 31 +++- .../loggedIn/notifications/CardFeedView.kt | 11 ++ .../dal/NotificationFeedFilter.kt | 2 + 6 files changed, 313 insertions(+), 4 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 5c2cbd4b5d..9266a50de8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -64,6 +65,8 @@ import androidx.compose.ui.window.PopupProperties import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder +import com.vitorpamplona.amethyst.commons.hashtags.Cashu +import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NoteState @@ -101,8 +104,11 @@ import com.vitorpamplona.amethyst.ui.theme.bitcoinColor import com.vitorpamplona.amethyst.ui.theme.overPictureBackground import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.time.ExperimentalTime @@ -182,6 +188,10 @@ private fun Galeries( DecryptAndRenderZapGallery(multiSetCard, backgroundColor, accountViewModel, nav) } + if (multiSetCard.nutzapEvents.isNotEmpty()) { + RenderNutzapGallery(multiSetCard.nutzapEvents, backgroundColor, accountViewModel, nav) + } + if (multiSetCard.boostEvents.isNotEmpty()) { RenderBoostGallery(multiSetCard.boostEvents, nav, accountViewModel) } @@ -340,6 +350,53 @@ fun RenderZapGallery( } } +@Composable +fun RenderNutzapGallery( + nutzapEvents: ImmutableList, + backgroundColor: MutableState, + accountViewModel: AccountViewModel, + nav: INav, +) { + // Convert each kind:9321 Note into the same shape AuthorGalleryZaps + // already renders for lightning. Amount comes from the parsed proof + // total (lazy via NutzapEvent.claimedSatsTotal), comment from the + // event's content. No NIP-44 decryption needed — nutzap doesn't + // have a private variant the way NIP-57 does. + val nutzapAuthorComments: ImmutableList = + remember(nutzapEvents) { + nutzapEvents + .map { note -> + val event = note.event as? NutzapEvent + val sats = event?.claimedSatsTotal() ?: 0L + ZapAmountCommentNotification( + user = note.author, + comment = event?.content?.ifBlank { null }, + amount = showAmount(java.math.BigDecimal(sats)), + ) + }.toImmutableList() + } + + Row(Modifier.fillMaxWidth()) { + Box( + modifier = WidthAuthorPictureModifier, + ) { + // CustomHashTagIcons.Cashu is a multi-tone branded glyph; + // tint=Unspecified keeps the brand colours instead of + // flattening to onBackground (which would lose the cashu- + // orange distinguishing the row from the lightning-bolt + // row above it). + Icon( + imageVector = CustomHashTagIcons.Cashu, + contentDescription = stringRes(R.string.nutzap), + modifier = Modifier.size(Size20dp).align(Alignment.TopEnd), + tint = Color.Unspecified, + ) + } + + AuthorGalleryZaps(nutzapAuthorComments, backgroundColor, nav, accountViewModel) + } +} + @Composable fun RenderBoostGallery( boostEvents: ImmutableList, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt new file mode 100644 index 0000000000..f060064a38 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NutzapUserSetCompose.kt @@ -0,0 +1,153 @@ +/* + * 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.note + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.hashtags.Cashu +import com.vitorpamplona.amethyst.commons.hashtags.CustomHashTagIcons +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NutzapUserSetCard +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer +import com.vitorpamplona.amethyst.ui.theme.Size25dp +import com.vitorpamplona.amethyst.ui.theme.Size55Modifier +import com.vitorpamplona.amethyst.ui.theme.Size55dp +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.claimedSatsTotal +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * Per-sender NIP-61 nutzap aggregate card — mirrors [ZapUserSetCompose] but + * renders the cashu icon instead of the lightning bolt so the user sees the + * rail at a glance. Built from the kind:9321 events directly: amount comes + * from each event's parsed proof total, sender from the event's pubkey. + * No NIP-44 decryption needed — nutzap is plaintext over the wire. + */ +@Composable +fun NutzapUserSetCompose( + nutzapSetCard: NutzapUserSetCard, + isInnerNote: Boolean = false, + routeForLastRead: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + val backgroundColor = + calculateBackgroundColor( + createdAt = nutzapSetCard.createdAt, + routeForLastRead = routeForLastRead, + accountViewModel = accountViewModel, + ) + + val authorComments: ImmutableList = + remember(nutzapSetCard.nutzapEvents) { + nutzapSetCard.nutzapEvents + .map { note -> + val event = note.event as? NutzapEvent + val sats = event?.claimedSatsTotal() ?: 0L + ZapAmountCommentNotification( + user = note.author, + comment = event?.content?.ifBlank { null }, + amount = showAmount(java.math.BigDecimal(sats)), + ) + }.toImmutableList() + } + + Column( + modifier = + Modifier + .background(backgroundColor.value) + .clickable { + nav.nav(routeFor(nutzapSetCard.user)) + }, + ) { + Row( + modifier = + Modifier.padding( + start = if (!isInnerNote) 12.dp else 0.dp, + end = if (!isInnerNote) 12.dp else 0.dp, + top = 10.dp, + ), + ) { + if (!isInnerNote) { + Box( + modifier = Size55Modifier, + ) { + // tint=Unspecified so the multi-tone cashu glyph keeps its + // brand colours, the same way it does in the zap chip popup. + Icon( + imageVector = CustomHashTagIcons.Cashu, + contentDescription = stringRes(R.string.nutzap), + modifier = Modifier.size(Size25dp).align(Alignment.TopEnd), + tint = Color.Unspecified, + ) + } + } + + Column(modifier = Modifier) { + Row(Modifier.fillMaxWidth()) { + AuthorGalleryZaps(authorComments, backgroundColor, nav, accountViewModel) + } + + Spacer(DoubleVertSpacer) + + Row( + Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp).fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + UserPicture( + nutzapSetCard.user, + Size55dp, + accountViewModel = accountViewModel, + nav = nav, + ) + + Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + UsernameDisplay(nutzapSetCard.user, accountViewModel = accountViewModel) + } + AboutDisplay(nutzapSetCard.user, accountViewModel) + } + } + + Spacer(DoubleVertSpacer) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt index 53c3730a3d..15292532f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedContentState.kt @@ -51,6 +51,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.collections.immutable.ImmutableList @@ -237,17 +238,46 @@ class CardFeedContentState( } } + // NIP-61 nutzaps — parallel to the lightning-zap grouping above. + // For nutzaps targeting a specific note, accumulate into + // [nutzapsPerEvent] so the MultiSetCard can show a cashu rail + // alongside the lightning rail. For nutzaps with no e-tagged + // target (just the recipient's p-tag), accumulate into + // [nutzapsPerUser] so they surface as a NutzapUserSetCard, + // mirroring how user-targeted lightning zaps become a + // ZapUserSetCard. + val nutzapsPerUser = mutableMapOf>() + val nutzapsPerEvent = mutableMapOf>() + notes + .filter { it.event is NutzapEvent } + .forEach { nutzapNote -> + val zappedPost = nutzapNote.replyTo?.lastOrNull() + if (zappedPost != null) { + nutzapsPerEvent + .getOrPut(zappedPost) { mutableListOf() } + .add(nutzapNote) + } else { + val sender = nutzapNote.author + if (sender != null) { + nutzapsPerUser + .getOrPut(sender) { mutableListOf() } + .add(nutzapNote) + } + } + } + val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat() - val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys + val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys + nutzapsPerEvent.keys val multiCards = allBaseNotes.flatMap { baseNote -> val boostsInCard = boostsPerEvent[baseNote] ?: emptyList() val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList() val zapsInCard = zapsPerEvent[baseNote] ?: emptyList() + val nutzapsInCard = nutzapsPerEvent[baseNote] ?: emptyList() val singleList = - (boostsInCard + zapsInCard.map { it.response } + reactionsInCard).groupBy { + (boostsInCard + zapsInCard.map { it.response } + reactionsInCard + nutzapsInCard).groupBy { sdf.format( Instant .ofEpochSecond(it.createdAt() ?: 0L) @@ -271,6 +301,7 @@ class CardFeedContentState( boostsInCard.filter { it in chunk }.toImmutableList(), reactionsInCard.filter { it in chunk }.toImmutableList(), zapsInCard.filter { it.response in chunk }.toImmutableList(), + nutzapsInCard.filter { it in chunk }.toImmutableList(), ) } }.flatten() @@ -299,6 +330,32 @@ class CardFeedContentState( } }.flatten() + // Per-sender nutzap aggregate cards — mirror userZaps but render + // with a cashu icon so the user sees the rail at a glance. Cards + // are bucketed by yyyy-MM-dd so a sender's daily run rolls up. + val userNutzaps = + nutzapsPerUser + .map { user -> + val byDay = + user.value.groupBy { + sdf.format( + Instant + .ofEpochSecond(it.createdAt() ?: 0L) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(), + ) + } + + byDay.values.map { nutzaps -> + NutzapUserSetCard( + user.key, + nutzaps + .sortedWith(compareByDescending { it.createdAt() }.thenBy { it.idHex }) + .toImmutableList(), + ) + } + }.flatten() + val textNoteCards = notes .filter { @@ -316,7 +373,7 @@ class CardFeedContentState( } } - return (multiCards + textNoteCards + userZaps) + return (multiCards + textNoteCards + userZaps + userNutzaps) .sortedWith(compareByDescending { it.createdAt() }.thenBy { it.id() }) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt index a4fab1dab4..3223aab655 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedState.kt @@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap @@ -60,18 +61,40 @@ class ZapUserSetCard( override fun id() = user.pubkeyHex + "U" + createdAt } +/** + * Per-sender NIP-61 nutzap aggregate, parallel to [ZapUserSetCard]. + * Carries raw kind:9321 [Note]s rather than [CombinedZap] pairs because + * a nutzap is a single event (no request/receipt split like lightning). + * Rendered with a cashu icon so the user can tell at a glance that it's + * a cashu rail, not a lightning rail. + */ +@Immutable +class NutzapUserSetCard( + val user: User, + val nutzapEvents: ImmutableList, +) : Card { + val createdAt = nutzapEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L + + override fun createdAt(): Long = createdAt + + // Suffix differs from ZapUserSetCard so the two never collide on id. + override fun id() = user.pubkeyHex + "N" + createdAt +} + @Immutable class MultiSetCard( val note: Note, val boostEvents: ImmutableList, val likeEvents: ImmutableList, val zapEvents: ImmutableList, + val nutzapEvents: ImmutableList = persistentListOf(), ) : Card { val maxCreatedAt = maxOf( zapEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L, likeEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L, boostEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L, + nutzapEvents.maxOfOrNull { it.createdAt() ?: 0L } ?: 0L, ) val minCreatedAt = @@ -79,6 +102,7 @@ class MultiSetCard( zapEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, likeEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, boostEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, + nutzapEvents.minOfOrNull { it.createdAt() ?: Long.MAX_VALUE } ?: Long.MAX_VALUE, ) val likeEventsByType = @@ -127,11 +151,16 @@ fun Card.containsEventId(eventId: String): Boolean = zapEvents.any { it.response.idHex == eventId || it.request.idHex == eventId } } + is NutzapUserSetCard -> { + nutzapEvents.any { it.idHex == eventId } + } + is MultiSetCard -> { note.idHex == eventId || zapEvents.any { it.response.idHex == eventId || it.request.idHex == eventId } || likeEvents.any { it.idHex == eventId } || - boostEvents.any { it.idHex == eventId } + boostEvents.any { it.idHex == eventId } || + nutzapEvents.any { it.idHex == eventId } } else -> { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt index 4c4fd18d4a..95be7bc938 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/CardFeedView.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.note.CloseIcon import com.vitorpamplona.amethyst.ui.note.MessageSetCompose import com.vitorpamplona.amethyst.ui.note.MultiSetCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose +import com.vitorpamplona.amethyst.ui.note.NutzapUserSetCompose import com.vitorpamplona.amethyst.ui.note.ZapUserSetCompose import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -290,6 +291,16 @@ private fun RenderCardItem( ) } + is NutzapUserSetCard -> { + NutzapUserSetCompose( + item, + isInnerNote = false, + accountViewModel = accountViewModel, + nav = nav, + routeForLastRead = routeForLastRead, + ) + } + is MultiSetCard -> { MultiSetCompose( item, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index 18800f93dc..9ef5cafd75 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -62,6 +62,7 @@ import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEven import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent +import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent import com.vitorpamplona.quartz.nip64Chess.challenge.accept.LiveChessGameAcceptEvent import com.vitorpamplona.quartz.nip64Chess.move.LiveChessMoveEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent @@ -128,6 +129,7 @@ class NotificationFeedFilter( ReactionEvent.KIND, RepostEvent.KIND, LnZapEvent.KIND, + NutzapEvent.KIND, OnchainZapEvent.KIND, LiveActivitiesChatMessageEvent.KIND, PictureEvent.KIND,