Merge pull request #2538 from vitorpamplona/claude/add-reply-notifications-GvMOn

Add reply and mention notifications for public notes
This commit is contained in:
Vitor Pamplona
2026-04-24 13:27:14 -04:00
committed by GitHub
11 changed files with 857 additions and 269 deletions
@@ -61,6 +61,23 @@ class WakeUpEvent(
fun kinds() = tags.kinds()
/**
* A WakeUpEvent's `p` tags point to the authors of the referenced subject
* events (see [authorKeys]), **not** to the account the wake-up should be
* delivered to. Example: Bob reacts to Alice's note, a WakeUpEvent about
* Bob's reaction p-tags Bob — but it's Alice's device that needs to wake
* up to process the reaction.
*
* WakeUpEvents reach this device through transport-level routing (push,
* relay subscription). By the time one lands in [LocalCache], it is
* already "for us" — so every logged-in signing account is a valid
* recipient to kick the relay wakeup on behalf of. Returning true here
* means the dispatcher invokes [com.vitorpamplona.quartz.experimental.
* notifications.wake.WakeUpEvent]-handling for each logged-in account,
* which is fine because keeping relay connections alive is idempotent.
*/
override fun notifies(userHex: HexKey): Boolean = true
companion object {
const val KIND = 23903
const val ALT_DESCRIPTION = "WakeUp"
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.core
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.EventKSerializer
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.serialization.Serializable
@@ -45,6 +46,24 @@ open class Event(
*/
open fun isContentEncoded() = false
/**
* Returns true when this event is intended to notify [userHex].
*
* The default delegates to
* [com.vitorpamplona.quartz.nip01Core.tags.people.PTag.isNotifying],
* i.e. "any lowercase `p` tag addresses the user" — the convention for
* most kinds that address a single recipient or a set of mentions
* (NIP-01 mentions, NIP-04/17 DMs, NIP-25 reactions, NIP-28 chat
* messages, NIP-34 git issues/patches, NIP-57 zap receipts, NIP-68
* pictures, NIP-71 videos, NIP-84 highlights, NIP-AC calls, chess,
* wiki/long-form/poll mentions).
*
* Subclasses override when the NIP defines additional notification tags
* — e.g. NIP-22 comments use uppercase `P` for the root author in
* addition to lowercase `p` for the direct-reply author.
*/
open fun notifies(userHex: HexKey): Boolean = PTag.isNotifying(tags, userHex)
fun toJson(): String = OptimizedJsonMapper.toJson(this)
companion object {
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.Tag
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
@@ -54,6 +55,24 @@ data class PTag(
key: HexKey,
): Boolean = tag.has(1) && tag[0] == TAG_NAME && tag[1] == key
/**
* Returns true if any `p` tag inside [tags] addresses [userHex].
* This is the canonical check for "does this event notify this user
* under the lowercase-`p` convention" and is the default used by
* [Event.notifies]. Kinds that address recipients through other tag
* names (e.g. NIP-22 uppercase `P` for root authors) override
* [Event.notifies] and may combine this with their own checks.
*/
fun isNotifying(
tags: TagArray,
userHex: HexKey,
): Boolean {
for (tag in tags) {
if (isTagged(tag, userHex)) return true
}
return false
}
fun parse(tag: Tag): PTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
@@ -197,6 +197,15 @@ class CommentEvent(
override fun unmarkedReplyTos() = emptyList<String>()
/**
* NIP-22 addresses two distinct recipients: the direct-reply author
* (lowercase `p` via [ReplyAuthorTag]) and the root-scope author
* (uppercase `P` via [RootAuthorTag]). A comment several levels deep
* only tags the root author with uppercase `P`, so the base-class
* lowercase-only default would miss them.
*/
override fun notifies(userHex: HexKey): Boolean = super.notifies(userHex) || rootAuthorKeys().contains(userHex)
override fun replyingTo(): HexKey? =
tags.lastNotNullOfOrNull(ReplyEventTag::parseKey)
?: tags.lastNotNullOfOrNull(RootEventTag::parseKey)