Use NIP-22 kind 1111 comments when replying to non-kind-1 events

Previously, clicking Reply on non-kind-1 events (treasures, themes, vines,
etc.) would create a kind 1 event with NIP-10 tags. NIP-22 specifies that
comments on non-kind-1 events must use kind 1111 with proper uppercase
(root) and lowercase (parent) tag structure.

- Detect non-kind-1 reply targets and switch to kind 1111
- Build NIP-22 tags: A/E/I + K + P for root scope, a/e/i + k + p for parent
- Handle addressable, replaceable, and regular event kinds
- Copy uppercase root tags from kind 1111 targets when replying to comments
- Invalidate comments cache instead of replies cache for NIP-22 events
This commit is contained in:
Alex Gleason
2026-02-27 18:20:14 -06:00
parent a25700126f
commit 53b8c0854e
+60 -6
View File
@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { Paperclip, Smile, AlertTriangle, X, Loader2 } from 'lucide-react';
import { nip19 } from 'nostr-tools';
import { encode as blurhashEncode } from 'blurhash';
import type { NostrEvent } from '@nostrify/nostrify';
import { NKinds, type NostrEvent } from '@nostrify/nostrify';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
@@ -487,9 +487,11 @@ export function ComposeBox({
tags.push(['p', pk]);
}
// NIP-10 reply tags
if (replyTo) {
// Determine root of the thread
// Reply tags: NIP-10 for kind 1 targets, NIP-22 for non-kind-1 targets
const isNip22Reply = replyTo && replyTo.kind !== 1;
if (replyTo && !isNip22Reply) {
// NIP-10 reply tags (kind 1 targets only)
const rootTag = replyTo.tags.find(([name, , , marker]) => name === 'e' && marker === 'root');
if (rootTag) {
// replyTo is itself a reply preserve the root and mark replyTo as reply
@@ -515,6 +517,55 @@ export function ComposeBox({
}
}
if (replyTo && isNip22Reply) {
// NIP-22 comment tags (non-kind-1 targets)
const dTag = replyTo.tags.find(([name]) => name === 'd')?.[1] ?? '';
if (replyTo.kind === 1111) {
// Replying to a kind 1111 comment: copy uppercase root tags from it
for (const tag of replyTo.tags) {
if (['A', 'E', 'I', 'K', 'P'].includes(tag[0])) {
tags.push([...tag]);
}
}
// Lowercase tags point to the parent comment
tags.push(['e', replyTo.id, DITTO_RELAY, replyTo.pubkey]);
tags.push(['k', '1111']);
tags.push(['p', replyTo.pubkey]);
} else {
// Replying directly to a non-kind-1 event: it is both root and parent
// Uppercase tags (root scope)
if (NKinds.addressable(replyTo.kind)) {
const addr = `${replyTo.kind}:${replyTo.pubkey}:${dTag}`;
tags.push(['A', addr, DITTO_RELAY]);
} else if (NKinds.replaceable(replyTo.kind)) {
const addr = `${replyTo.kind}:${replyTo.pubkey}:`;
tags.push(['A', addr, DITTO_RELAY]);
} else {
tags.push(['E', replyTo.id, DITTO_RELAY, replyTo.pubkey]);
}
tags.push(['K', replyTo.kind.toString()]);
tags.push(['P', replyTo.pubkey]);
// Lowercase tags (parent = same as root for top-level comments)
if (NKinds.addressable(replyTo.kind)) {
const addr = `${replyTo.kind}:${replyTo.pubkey}:${dTag}`;
tags.push(['a', addr, DITTO_RELAY]);
// Also include an e tag referencing the event id for replaceable/addressable events
tags.push(['e', replyTo.id, DITTO_RELAY, replyTo.pubkey]);
} else if (NKinds.replaceable(replyTo.kind)) {
const addr = `${replyTo.kind}:${replyTo.pubkey}:`;
tags.push(['a', addr, DITTO_RELAY]);
tags.push(['e', replyTo.id, DITTO_RELAY, replyTo.pubkey]);
} else {
tags.push(['e', replyTo.id, DITTO_RELAY, replyTo.pubkey]);
}
tags.push(['k', replyTo.kind.toString()]);
tags.push(['p', replyTo.pubkey]);
}
}
// Quote tags (if quoted event and not removed)
// Per NIP-18, quotes should use the q tag and include the nostr: URI in content
let finalContent = content.trim();
@@ -598,7 +649,7 @@ export function ComposeBox({
await createEvent({
kind: 1,
kind: isNip22Reply ? 1111 : 1,
content: finalContent,
tags,
created_at: Math.floor(Date.now() / 1000),
@@ -614,7 +665,10 @@ export function ComposeBox({
setWebxdcUuids(new Map());
setWebxdcMetas(new Map());
queryClient.invalidateQueries({ queryKey: ['feed'] });
if (replyTo) {
if (replyTo && isNip22Reply) {
// Invalidate NIP-22 comments cache
queryClient.invalidateQueries({ queryKey: ['nostr', 'comments'] });
} else if (replyTo) {
queryClient.invalidateQueries({ queryKey: ['replies', replyTo.id] });
}
if (quotedEvent) {