Merge pull request #1024 from PR0M3TH3AN/codex/modify-comment-tag-handling-in-nostreventschemas

Ensure comment builder keeps root #e tags with address pointers
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-11-03 11:54:22 -05:00
commit eda229b3d6
7 files changed
+140 -13

No files matched your search

+2
View File
@@ -141,6 +141,8 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
| Video comment (`NOTE_TYPES.VIDEO_COMMENT`) | `1111` | NIP-22 root scope tags `['A'\|`E`\|`I`, <pointer>, <relay?>?], `['K', <root kind>]`, and `['P', <root author pubkey>, <relay?>?]` plus parent metadata `['a'\|`e`\|`i`, <parent pointer>, <relay?>?, <author?>], `['k', <parent kind>]`, `['p', <parent author>, <relay?>?]`; builder retains legacy lowercase fallbacks (`['a', ...]`/`['e', ...]`/`['p', ...]`) alongside the uppercase set for compatibility; inherits schema append tags | Plain text body sanitized to valid UTF-8 |
> **Publishing note:** Session actors only emit passive telemetry (for example, view counters) and must **not** sign video comments. Require a logged-in Nostr signer for comment publishing via [`commentEvents`](../js/nostr/commentEvents.js).
> **Compatibility tip:** When a comment anchors itself to a video definition address, emit both the uppercase root pointer and a legacy `['e', <root event id>]` tag. This mirrors the [NIP-22 addressable events example](./nips/22.md#addressable-events) so clients that only understand `#e` tags can still follow the thread.
| NIP-71 video (`NOTE_TYPES.NIP71_VIDEO`) | `21` | `['title', <title>]`, optional `['published_at', <unix seconds>]`, optional `['alt', <text>]`, repeated `['imeta', ...]` entries describing NIP-92 media variants, optional `['duration', <seconds>]`, repeated `['text-track', <url>, <kind>, <language>]`, optional `['content-warning', <reason>]`, repeated `['segment', <start>, <end>, <title>, <thumbnail>]`, repeated hashtags `['t', <tag>]`, repeated participants `['p', <pubkey>, <relay?>]`, repeated references `['r', <url>]` | Plain text summary carried in the content field. Publishing is gated by the `FEATURE_PUBLISH_NIP71` runtime flag while the rollout stabilizes. |
| NIP-71 short video (`NOTE_TYPES.NIP71_SHORT_VIDEO`) | `22` | Same as `NOTE_TYPES.NIP71_VIDEO`; the kind differentiates short-form presentations. | Plain text summary; gated by `FEATURE_PUBLISH_NIP71`. |
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
+3 -5
View File
@@ -1160,12 +1160,10 @@ export function buildCommentEvent({
}
}
const includeVideoEventTag =
!normalizedVideoDefinitionAddress &&
Boolean(videoEventTagName) &&
Boolean(normalizedVideoEventId);
const shouldAppendVideoEventTag =
Boolean(videoEventTagName) && Boolean(normalizedVideoEventId);
if (includeVideoEventTag) {
if (shouldAppendVideoEventTag) {
appendPointerTag(
videoEventTagName,
normalizedVideoEventId,
+12 -2
View File
@@ -144,7 +144,12 @@ export default class VideoModalCommentController {
}
const videoEventId = normalizeString(video.id);
const videoDefinitionAddress = buildVideoAddressPointer(video);
let videoDefinitionAddress = buildVideoAddressPointer(video);
if (!videoDefinitionAddress && this.modalCommentState.videoDefinitionAddress) {
videoDefinitionAddress = normalizeString(
this.modalCommentState.videoDefinitionAddress,
);
}
const videoKind = normalizeKind(video.kind);
const videoAuthorPubkey = normalizeString(video.pubkey);
@@ -544,7 +549,12 @@ export default class VideoModalCommentController {
}
const videoEventId = normalizeString(video.id);
const videoDefinitionAddress = buildVideoAddressPointer(video);
let videoDefinitionAddress = buildVideoAddressPointer(video);
if (!videoDefinitionAddress && this.modalCommentState.videoDefinitionAddress) {
videoDefinitionAddress = normalizeString(
this.modalCommentState.videoDefinitionAddress,
);
}
const videoKind =
this.modalCommentState.videoKind || normalizeKind(video.kind);
const videoAuthorPubkey =
+3
View File
@@ -25,6 +25,7 @@ test("buildCommentEvent emits NIP-22 root metadata while keeping legacy fallback
["A", "30078:deadbeefcafebabe:clip-1"],
["K", "30078"],
["P", "deadbeefcafebabe"],
["e", "video-event-id"],
["a", "30078:deadbeefcafebabe:clip-1"],
["k", "30078"],
["p", "deadbeefcafebabe"],
@@ -48,6 +49,7 @@ test("buildCommentEvent includes parent pointers, kinds, and authors for replies
["A", "30078:deadbeefcafebabe:clip-1"],
["K", "30078"],
["P", "deadbeefcafebabe"],
["e", "video-event-id"],
["a", "30078:deadbeefcafebabe:clip-1"],
["e", "parent-comment-id", "parent-author"],
["k", String(VIDEO_COMMENT_KIND)],
@@ -76,6 +78,7 @@ test("buildCommentEvent normalizes relays and preserves explicit overrides", ()
["A", "30078:deadbeefcafebabe:clip-2", "wss://video.def"],
["K", "30078"],
["P", "deadbeefcafebabe", "wss://video.def"],
["e", "event123", "wss://comments.main"],
["a", "30078:deadbeefcafebabe:clip-2", "wss://video.def"],
["e", "root-comment", "wss://parent", "cafecafe"],
["k", String(VIDEO_COMMENT_KIND)],
+36
View File
@@ -62,6 +62,24 @@ test(
}),
};
assert.ok(
topLevelEvent.tags.some(
(tag) =>
Array.isArray(tag) &&
tag[0] === "e" &&
tag[1] === videoEventId &&
tag[2] === relayUrl,
),
"builder should retain legacy #e tag for the root event when address pointer is present",
);
assert.ok(
topLevelEvent.tags.some(
(tag) =>
Array.isArray(tag) && tag[0] === "a" && tag[1] === videoDefinitionAddress,
),
"builder should include #a pointer for the scoped video definition",
);
const replyEvent = {
id: replyCommentId,
...buildCommentEvent({
@@ -81,6 +99,24 @@ test(
}),
};
assert.ok(
replyEvent.tags.some(
(tag) =>
Array.isArray(tag) &&
tag[0] === "e" &&
tag[1] === videoEventId &&
tag[2] === relayUrl,
),
"reply builder should include root #e tag alongside #a pointer",
);
assert.ok(
replyEvent.tags.some(
(tag) =>
Array.isArray(tag) && tag[0] === "a" && tag[1] === videoDefinitionAddress,
),
"reply builder should preserve #a pointer",
);
const listCalls = [];
const client = {
relays: [relayUrl],
+2
View File
@@ -115,6 +115,7 @@ test("publishComment prefers active signer when available", async () => {
["A", "30078:deadbeef:clip", "wss://definition-relay"],
["K", "30078"],
["P", "deadbeef", "wss://definition-relay"],
["e", "video-event-id", "wss://video-relay"],
["a", "30078:deadbeef:clip", "wss://definition-relay"],
["e", "parent-comment-id", "thread-participant"],
["k", String(COMMENT_EVENT_KIND)],
@@ -239,6 +240,7 @@ test("publishComment derives root and parent metadata from parent comment tags",
["A", "30078:videopk:root", "wss://definition"],
["K", "30078"],
["P", "videopk", "wss://author"],
["e", "video-event", "wss://root"],
["a", "30078:videopk:root", "wss://definition"],
["e", "parent-comment", "wss://parent", "parentpk"],
["k", String(COMMENT_EVENT_KIND)],
+82 -6
View File
@@ -26,6 +26,17 @@ function createCommentEvent({ id, pubkey, content, createdAt, parentId = null })
if (parentId) {
tags.push(["e", parentId]);
}
assert.ok(
tags.some((tag) => Array.isArray(tag) && tag[0] === "e" && tag[1] === "video123"),
"comment fixture should include root #e tag",
);
assert.ok(
tags.some(
(tag) =>
Array.isArray(tag) && tag[0] === "a" && tag[1] === "30078:authorpk:video-root",
),
"comment fixture should include root #a tag",
);
return {
id,
pubkey,
@@ -213,12 +224,13 @@ test(
},
});
const video = {
id: "video123",
pubkey: "AUTHORPK",
enableComments: true,
kind: 30078,
};
const video = {
id: "video123",
pubkey: "AUTHORPK",
enableComments: true,
kind: 30078,
tags: [["d", "video-root"]],
};
controller.load(video);
controller.submit({ text: "Nice" });
@@ -229,6 +241,70 @@ test(
const payload = publishCalls[0];
assert.equal(payload.videoEventId, "video123");
assert.equal(payload.videoAuthorPubkey, "AUTHORPK");
assert.equal(payload.videoDefinitionAddress, "30078:AUTHORPK:video-root");
},
);
test(
"VideoModalCommentController falls back to thread pointer when video tags are absent",
async () => {
const publishCalls = [];
const controller = new VideoModalCommentController({
commentThreadService: {
setCallbacks: () => {},
teardown: () => {},
defaultLimit: 10,
loadThread: () => Promise.resolve(),
processIncomingEvent: () => {},
},
videoModal: {
setCommentSectionCallbacks: () => {},
hideCommentsDisabledMessage: () => {},
showCommentsDisabledMessage: () => {},
setCommentsVisibility: () => {},
clearComments: () => {},
resetCommentComposer: () => {},
setCommentStatus: () => {},
setCommentComposerState: () => {},
},
auth: {
isLoggedIn: () => true,
},
services: {
publishComment: async (payload) => {
publishCalls.push(payload);
return { ok: true, event: { id: "comment", tags: [] } };
},
},
utils: {
normalizeHexPubkey: (value) => value,
},
});
const video = {
id: "video456",
pubkey: "AUTHORPK",
enableComments: true,
kind: 30078,
// No tags provided; buildVideoAddressPointer should return an empty string.
};
controller.load(video);
controller.modalCommentState.videoDefinitionAddress =
" 30078:AUTHORPK:video-root ";
controller.submit({ text: "Fallback pointer" });
await controller.modalCommentPublishPromise;
assert.equal(publishCalls.length, 1);
const payload = publishCalls[0];
assert.equal(
payload.videoDefinitionAddress,
"30078:AUTHORPK:video-root",
"publish payload should reuse pointer from the loaded thread",
);
},
);