mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
fix: replies to likes and zaps now reach the Curated notification feed
The Curated (Selected) notification mode gates events through tagsAnEventByUser, which only recognized a reply by its parent note's author in the local cache. That made replies to the user's reactions disappear whenever the reaction event wasn't cached (nothing re-fetches old kind 7s after a restart), and replies to the user's zaps never matched at all because zap receipts are authored by the lightning provider. Detect both from the kind 1111 event itself: reaction parents via the NIP-22 root/reply author tags, zap parents via the k tag plus the reply's explicit p tag on the user. Covered by NotificationTagsAnEventByUserTest. Also renders the embedded target note inside zap and reaction cards with makeItShort, keeping the inner preview compact. https://claude.ai/code/session_01LM3KTECMMAdNBHZfs1dANa
This commit is contained in:
@@ -43,6 +43,7 @@ fun RenderReaction(
|
||||
it,
|
||||
modifier = Modifier,
|
||||
isBoostedNote = true,
|
||||
makeItShort = true,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
|
||||
@@ -79,6 +79,7 @@ fun RenderZappedPost(
|
||||
it,
|
||||
modifier = Modifier,
|
||||
isBoostedNote = true,
|
||||
makeItShort = true,
|
||||
unPackReply = ReplyRenderType.NONE,
|
||||
quotesLeft = quotesLeft - 1,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
|
||||
+23
@@ -168,6 +168,29 @@ class NotificationFeedFilter(
|
||||
return true
|
||||
}
|
||||
|
||||
if (event is CommentEvent) {
|
||||
// NIP-22 comments carry their root/parent authors as tags, so a
|
||||
// reply to the user's reaction stays detectable even when the
|
||||
// reaction event is not in the local cache (the replyTo-author
|
||||
// check above needs it loaded).
|
||||
if (event.rootAuthorKeys().contains(authorHex) || event.replyAuthorKeys().contains(authorHex)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Replies to the user's zaps: the receipt — and therefore the
|
||||
// author tags above — is signed by the recipient's lightning
|
||||
// provider, not the zapper. The `k` tag (or the cached parent)
|
||||
// proves the comment targets a zap; the reply's explicit p tag
|
||||
// on the user marks it as theirs.
|
||||
val targetsZapReceipt =
|
||||
event.hasScopeKind(LnZapEvent.KIND.toString()) ||
|
||||
note.replyTo?.any { it.event is LnZapEvent } == true
|
||||
|
||||
if (targetsZapReceipt && event.isTaggedUser(authorHex)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if ((event is TextNoteEvent || event is CommentEvent)) {
|
||||
val community =
|
||||
event
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.notifications.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* The "Curated" (Selected) notification mode gates every event through
|
||||
* [NotificationFeedFilter.tagsAnEventByUser]. Replies to the user's likes and
|
||||
* zaps are kind:1111 comments whose relevance must be detectable without the
|
||||
* parent event in the local cache: reaction parents via the NIP-22 root/reply
|
||||
* author tags, zap parents via the `k` tag plus the explicit p tag (the
|
||||
* receipt itself is signed by the lightning provider, not the zapper).
|
||||
*/
|
||||
class NotificationTagsAnEventByUserTest {
|
||||
private val me = "1".repeat(64)
|
||||
private val wallet = "2".repeat(64)
|
||||
private val replier = "3".repeat(64)
|
||||
private val reactionId = "a".repeat(64)
|
||||
private val zapId = "b".repeat(64)
|
||||
private val replyId = "c".repeat(64)
|
||||
private val sig = "f".repeat(128)
|
||||
|
||||
@Test
|
||||
fun `reply to my reaction is relevant via nip22 author tags without the parent in cache`() {
|
||||
val reply =
|
||||
CommentEvent(
|
||||
replyId,
|
||||
replier,
|
||||
1000,
|
||||
arrayOf(
|
||||
arrayOf("E", reactionId, "", me),
|
||||
arrayOf("K", "7"),
|
||||
arrayOf("P", me),
|
||||
arrayOf("e", reactionId, "", me),
|
||||
arrayOf("k", "7"),
|
||||
arrayOf("p", me),
|
||||
),
|
||||
"lol same",
|
||||
sig,
|
||||
)
|
||||
// Parent reaction NOT in cache: replyTo resolves to an empty note (no author).
|
||||
val note =
|
||||
Note(replyId).apply {
|
||||
event = reply
|
||||
replyTo = listOf(Note(reactionId))
|
||||
}
|
||||
|
||||
assertTrue(NotificationFeedFilter.tagsAnEventByUser(note, me))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reply to my zap is relevant via k tag and explicit p tag without the receipt in cache`() {
|
||||
val reply =
|
||||
CommentEvent(
|
||||
replyId,
|
||||
replier,
|
||||
1000,
|
||||
arrayOf(
|
||||
arrayOf("E", zapId, "", wallet),
|
||||
arrayOf("K", LnZapEvent.KIND.toString()),
|
||||
arrayOf("P", wallet),
|
||||
arrayOf("e", zapId, "", wallet),
|
||||
arrayOf("k", LnZapEvent.KIND.toString()),
|
||||
arrayOf("p", wallet),
|
||||
arrayOf("p", me),
|
||||
),
|
||||
"thanks for the zap!",
|
||||
sig,
|
||||
)
|
||||
val note =
|
||||
Note(replyId).apply {
|
||||
event = reply
|
||||
replyTo = listOf(Note(zapId))
|
||||
}
|
||||
|
||||
assertTrue(NotificationFeedFilter.tagsAnEventByUser(note, me))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reply to someone else's zap that does not tag me stays irrelevant`() {
|
||||
val reply =
|
||||
CommentEvent(
|
||||
replyId,
|
||||
replier,
|
||||
1000,
|
||||
arrayOf(
|
||||
arrayOf("E", zapId, "", wallet),
|
||||
arrayOf("K", LnZapEvent.KIND.toString()),
|
||||
arrayOf("P", wallet),
|
||||
arrayOf("e", zapId, "", wallet),
|
||||
arrayOf("k", LnZapEvent.KIND.toString()),
|
||||
arrayOf("p", wallet),
|
||||
),
|
||||
"nice zap",
|
||||
sig,
|
||||
)
|
||||
val note =
|
||||
Note(replyId).apply {
|
||||
event = reply
|
||||
replyTo = listOf(Note(zapId))
|
||||
}
|
||||
|
||||
assertFalse(NotificationFeedFilter.tagsAnEventByUser(note, me))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user