diff --git a/docs/nostr-event-schemas.md b/docs/nostr-event-schemas.md index d23d32da..9bb91cdf 100644 --- a/docs/nostr-event-schemas.md +++ b/docs/nostr-event-schemas.md @@ -137,11 +137,10 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca | Video post (`NOTE_TYPES.VIDEO_POST`) | `30078` | `['t','video']`, `['d', ]` plus optional schema append tags | JSON payload using Content Schema v3 (`version`, `title`, optional `url`, `magnet`, `thumbnail`, `description`, `mode`, `videoRootId`, `deleted`, `isPrivate`, `isNsfw`, `isForKids`, `enableComments`, `ws`, `xs`) | | NIP-94 mirror (`NOTE_TYPES.VIDEO_MIRROR`) | `1063` | Tags forwarded from `publishVideo` (URL, mime type, thumbnail, alt text, magnet) | Plain text alt description | | Repost (`NOTE_TYPES.REPOST`) | `6` | `['e', , ]` with optional address pointer `['a', , ]`, and `['p', ]` when the origin author is known; inherits schema append tags | Empty content | -| Video comment (`NOTE_TYPES.VIDEO_COMMENT`) | `1111` | NIP-22 root scope tags `['A'\|`E`\|`I`, , ?], `['K', ]`, and `['P', , ?]` plus parent metadata `['a'\|`e`\|`i`, , ?, ], `['k', ]`, `['p', , ?]`; 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 | +| Video comment (`NOTE_TYPES.VIDEO_COMMENT`) | `1111` | NIP-22 root scope tags `['A'\|`E`\|`I`, , ?], `['K', ]`, and `['P', , ?]` plus parent metadata `['E'\|`I`, , ?, ], `['K', ]`, `['P', , ?]`; 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', ]` 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', ]`, 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 | diff --git a/js/nostr/commentEvents.js b/js/nostr/commentEvents.js index 1943fa1c..2d46f4f9 100644 --- a/js/nostr/commentEvents.js +++ b/js/nostr/commentEvents.js @@ -799,16 +799,10 @@ function createVideoCommentFilters(targetInput, options = {}) { const filters = []; const eventFilter = { - kinds: getAllowedCommentKinds(), - "#e": [descriptor.videoEventId], - }; - filters.push(applyFilterOptions(eventFilter, options)); - - const uppercaseEventFilter = { kinds: getAllowedCommentKinds(), "#E": [descriptor.videoEventId], }; - filters.push(applyFilterOptions(uppercaseEventFilter, options)); + filters.push(applyFilterOptions(eventFilter, options)); const uppercaseFilter = { kinds: getAllowedCommentKinds() }; let hasUppercasePointer = false; @@ -817,12 +811,6 @@ function createVideoCommentFilters(targetInput, options = {}) { uppercaseFilter["#I"] = [descriptor.rootIdentifier]; hasUppercasePointer = true; - const lowercaseRootFilter = { - kinds: getAllowedCommentKinds(), - "#i": [descriptor.rootIdentifier], - }; - - filters.push(applyFilterOptions(lowercaseRootFilter, options)); } else if ( typeof descriptor.videoDefinitionAddress === "string" && descriptor.videoDefinitionAddress @@ -859,12 +847,6 @@ function createVideoCommentFilters(targetInput, options = {}) { descriptor.parentCommentId && descriptor.parentCommentId !== descriptor.videoEventId ) { - const parentFilter = { - kinds: getAllowedCommentKinds(), - "#e": [descriptor.parentCommentId], - }; - filters.push(applyFilterOptions(parentFilter, options)); - const parentUppercaseFilter = { kinds: getAllowedCommentKinds(), "#E": [descriptor.parentCommentId], @@ -873,17 +855,6 @@ function createVideoCommentFilters(targetInput, options = {}) { } if (descriptor.videoDefinitionAddress) { - const definitionFilter = { - kinds: getAllowedCommentKinds(), - "#a": [descriptor.videoDefinitionAddress], - }; - - if (descriptor.parentCommentId) { - definitionFilter["#e"] = [descriptor.parentCommentId]; - } - - filters.push(applyFilterOptions(definitionFilter, options)); - const definitionUppercaseFilter = { kinds: getAllowedCommentKinds(), "#A": [descriptor.videoDefinitionAddress], @@ -976,21 +947,8 @@ function isVideoCommentEvent(event, descriptor) { targetDescriptor.parentAuthorPubkey, ); - let sawRootKindTag = false; - let rootKindMatches = !expectedRootKind; - let rootKindMismatch = false; - - let sawRootAuthorTag = false; - let rootAuthorMatches = !expectedRootAuthor; - let rootAuthorMismatch = false; - - let sawParentKindTag = false; - let parentKindMatches = !expectedParentKind; - let parentKindMismatch = false; - - let sawParentAuthorTag = false; - let parentAuthorMatches = !expectedParentAuthor; - let parentAuthorMismatch = false; + const kindTagValues = []; + const authorTagValues = []; for (const tag of tags) { if (!Array.isArray(tag) || tag.length < 2) { @@ -998,20 +956,20 @@ function isVideoCommentEvent(event, descriptor) { } const [rawName, rawValue] = tag; const trimmedName = typeof rawName === "string" ? rawName.trim() : ""; - const lowerName = trimmedName.toLowerCase(); + const upperName = trimmedName.toUpperCase(); const normalizedValue = normalizeTagValue(rawValue); - if (!lowerName || !normalizedValue) { + if (!upperName || !normalizedValue) { continue; } - if (lowerName === "i") { + if (upperName === "I") { if ( normalizedRootIdentifier && normalizedValue === normalizedRootIdentifier ) { hasIdentifierTag = true; } - } else if (lowerName === "a") { + } else if (upperName === "A") { if ( normalizedVideoDefinitionAddress && normalizedValue === normalizedVideoDefinitionAddress @@ -1019,7 +977,7 @@ function isVideoCommentEvent(event, descriptor) { hasDefinitionTag = true; matchedDefinitionPointer = true; } - } else if (lowerName === "e") { + } else if (upperName === "E") { if ( normalizedVideoEventId && normalizedValue === normalizedVideoEventId @@ -1033,37 +991,28 @@ function isVideoCommentEvent(event, descriptor) { ) { hasParentTag = true; } - } else if (trimmedName === "K") { - sawRootKindTag = true; - if (!expectedRootKind || normalizedValue === expectedRootKind) { - rootKindMatches = true; - } else { - rootKindMismatch = true; - } - } else if (trimmedName === "P") { - sawRootAuthorTag = true; - if (!expectedRootAuthor || normalizedValue === expectedRootAuthor) { - rootAuthorMatches = true; - } else { - rootAuthorMismatch = true; - } - } else if (trimmedName === "k") { - sawParentKindTag = true; - if (!expectedParentKind || normalizedValue === expectedParentKind) { - parentKindMatches = true; - } else { - parentKindMismatch = true; - } - } else if (trimmedName === "p") { - sawParentAuthorTag = true; - if (!expectedParentAuthor || normalizedValue === expectedParentAuthor) { - parentAuthorMatches = true; - } else { - parentAuthorMismatch = true; - } + } else if (upperName === "K") { + kindTagValues.push(normalizedValue); + } else if (upperName === "P") { + authorTagValues.push(normalizedValue); } } + const sawRootKindTag = kindTagValues.length > 0 && Boolean(expectedRootKind); + const sawRootAuthorTag = authorTagValues.length > 0 && Boolean(expectedRootAuthor); + const sawParentKindTag = kindTagValues.length > 0 && Boolean(expectedParentKind); + const sawParentAuthorTag = + authorTagValues.length > 0 && Boolean(expectedParentAuthor); + + const rootKindMatches = + !expectedRootKind || kindTagValues.includes(expectedRootKind); + const rootAuthorMatches = + !expectedRootAuthor || authorTagValues.includes(expectedRootAuthor); + const parentKindMatches = + !expectedParentKind || kindTagValues.includes(expectedParentKind); + const parentAuthorMatches = + !expectedParentAuthor || authorTagValues.includes(expectedParentAuthor); + if (!hasDefinitionTag && requiresAddressMatch && matchedEventPointer) { hasDefinitionTag = true; } @@ -1107,14 +1056,6 @@ function isVideoCommentEvent(event, descriptor) { return false; } - if ((sawRootKindTag && rootKindMismatch) || (sawRootAuthorTag && rootAuthorMismatch)) { - return false; - } - - if ((sawParentKindTag && parentKindMismatch) || (sawParentAuthorTag && parentAuthorMismatch)) { - return false; - } - if (sawRootKindTag && expectedRootKind && !rootKindMatches) { return false; } diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index b584ff29..582ddbf0 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -334,16 +334,16 @@ const BASE_SCHEMAS = { type: NOTE_TYPES.VIDEO_COMMENT, label: "Video comment", kind: 1111, - videoEventTagName: "e", - videoDefinitionTagName: "a", - parentCommentTagName: "e", - parentAuthorTagName: "p", + videoEventTagName: "E", + videoDefinitionTagName: "A", + parentCommentTagName: "E", + parentAuthorTagName: "P", rootEventPointerTagName: "E", rootDefinitionPointerTagName: "A", rootIdentifierPointerTagName: "I", rootKindTagName: "K", rootAuthorTagName: "P", - parentKindTagName: "k", + parentKindTagName: "K", appendTags: DEFAULT_APPEND_TAGS, content: { format: "text", @@ -1199,11 +1199,12 @@ export function buildCommentEvent({ const rootIdentifierPointerTagName = schema?.rootIdentifierPointerTagName || "I"; const rootKindTagName = schema?.rootKindTagName || "K"; const rootAuthorTagName = schema?.rootAuthorTagName || "P"; - const videoDefinitionTagName = schema?.videoDefinitionTagName || "a"; - const videoEventTagName = schema?.videoEventTagName || "e"; - const parentCommentTagName = schema?.parentCommentTagName || "e"; - const parentAuthorTagName = schema?.parentAuthorTagName || "p"; - const parentKindTagName = schema?.parentKindTagName || "k"; + const parentIdentifierTagName = schema?.parentIdentifierTagName || "I"; + const videoDefinitionTagName = schema?.videoDefinitionTagName || "A"; + const videoEventTagName = schema?.videoEventTagName || "E"; + const parentCommentTagName = schema?.parentCommentTagName || "E"; + const parentAuthorTagName = schema?.parentAuthorTagName || "P"; + const parentKindTagName = schema?.parentKindTagName || "K"; const appendPointerTag = (tagName, value, relay, authorHint) => { if (!tagName || !value) { @@ -1272,7 +1273,11 @@ export function buildCommentEvent({ } if (normalizedParentIdentifier) { - appendPointerTag("i", normalizedParentIdentifier, normalizedParentIdentifierRelay); + appendPointerTag( + parentIdentifierTagName, + normalizedParentIdentifier, + normalizedParentIdentifierRelay, + ); } if (parentCommentTagName && normalizedParentCommentId) { diff --git a/js/services/discussionCountService.js b/js/services/discussionCountService.js index 6ddca0e2..a47e3020 100644 --- a/js/services/discussionCountService.js +++ b/js/services/discussionCountService.js @@ -131,7 +131,7 @@ export default class DiscussionCountService { const filters = []; const eventId = typeof video.id === "string" ? video.id.trim() : ""; if (eventId) { - filters.push({ kinds: [COMMENT_EVENT_KIND], "#e": [eventId] }); + filters.push({ kinds: [COMMENT_EVENT_KIND], "#E": [eventId] }); } const address = buildVideoAddressPointer(video); @@ -181,7 +181,7 @@ export default class DiscussionCountService { } if (address) { - filters.push({ kinds: [COMMENT_EVENT_KIND], "#a": [address] }); + filters.push({ kinds: [COMMENT_EVENT_KIND], "#A": [address] }); } return filters; diff --git a/tests/comment-event-builder.test.mjs b/tests/comment-event-builder.test.mjs index b1f80962..3a27cee5 100644 --- a/tests/comment-event-builder.test.mjs +++ b/tests/comment-event-builder.test.mjs @@ -48,10 +48,9 @@ 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"], + ["E", "video-event-id"], + ["A", "30078:deadbeefcafebabe:clip-1"], + ["K", "30078"], ]); assert.equal(event.content, "Great video!"); }); @@ -72,11 +71,11 @@ 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)], - ["p", "parent-author"], + ["E", "video-event-id"], + ["A", "30078:deadbeefcafebabe:clip-1"], + ["E", "parent-comment-id", "parent-author"], + ["K", String(VIDEO_COMMENT_KIND)], + ["P", "parent-author"], ]); }); @@ -101,11 +100,11 @@ 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)], - ["p", "cafecafe", "wss://profile"], + ["E", "event123", "wss://comments.main"], + ["A", "30078:deadbeefcafebabe:clip-2", "wss://video.def"], + ["E", "root-comment", "wss://parent", "cafecafe"], + ["K", String(VIDEO_COMMENT_KIND)], + ["P", "cafecafe", "wss://profile"], ["client", "bitvid"], ]); assert.equal(event.content, " Appreciated! "); @@ -128,9 +127,9 @@ test("buildCommentEvent falls back to event pointers when no address is supplied ["E", "legacy-event", "wss://legacy.example", "legacy-author"], ["K", "1063"], ["P", "legacy-author"], - ["e", "legacy-event", "wss://legacy.example"], - ["k", "1063"], - ["p", "legacy-pubkey"], + ["E", "legacy-event", "wss://legacy.example"], + ["K", "1063"], + ["P", "legacy-pubkey"], ]); }); @@ -152,10 +151,10 @@ test("buildCommentEvent accepts partial metadata for parent overrides", () => { assert.deepStrictEqual(event.tags, [ ["E", "file-event"], ["K", "1063"], - ["e", "file-event"], - ["e", "parent-comment", "wss://parent", "parent-only"], - ["k", "1111"], - ["p", "parent-only", "wss://parent-author"], + ["E", "file-event"], + ["E", "parent-comment", "wss://parent", "parent-only"], + ["K", "1111"], + ["P", "parent-only", "wss://parent-author"], ]); }); diff --git a/tests/comment-thread-service.test.mjs b/tests/comment-thread-service.test.mjs index d2481111..11e260d0 100644 --- a/tests/comment-thread-service.test.mjs +++ b/tests/comment-thread-service.test.mjs @@ -608,18 +608,18 @@ test( topLevelEvent.tags.some( (tag) => Array.isArray(tag) && - tag[0] === "e" && + tag[0] === "E" && tag[1] === videoEventId && tag[2] === relayUrl, ), - "builder should retain legacy #e tag for the root event when address pointer is present", + "builder should retain uppercase #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, + Array.isArray(tag) && tag[0] === "A" && tag[1] === videoDefinitionAddress, ), - "builder should include #a pointer for the scoped video definition", + "builder should include #A pointer for the scoped video definition", ); const replyEvent = { @@ -645,18 +645,18 @@ test( replyEvent.tags.some( (tag) => Array.isArray(tag) && - tag[0] === "e" && + tag[0] === "E" && tag[1] === videoEventId && tag[2] === relayUrl, ), - "reply builder should include root #e tag alongside #a pointer", + "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, + Array.isArray(tag) && tag[0] === "A" && tag[1] === videoDefinitionAddress, ), - "reply builder should preserve #a pointer", + "reply builder should preserve #A pointer", ); const listCalls = []; diff --git a/tests/discussion-count-service.test.mjs b/tests/discussion-count-service.test.mjs index 8f94dbd0..aa2fb5ec 100644 --- a/tests/discussion-count-service.test.mjs +++ b/tests/discussion-count-service.test.mjs @@ -40,7 +40,7 @@ async function testRefreshCountsCachesResults() { const [eventFilter, uppercaseFilter, addressFilter] = filters; assert.deepEqual(eventFilter, { kinds: [COMMENT_EVENT_KIND], - "#e": ["count-video-1"], + "#E": ["count-video-1"], }); assert.deepEqual(uppercaseFilter, { kinds: [COMMENT_EVENT_KIND], @@ -50,7 +50,7 @@ async function testRefreshCountsCachesResults() { }); assert.deepEqual(addressFilter, { kinds: [COMMENT_EVENT_KIND], - "#a": ["30078:pubkey-1:identifier-1"], + "#A": ["30078:pubkey-1:identifier-1"], }); return { total: 5, @@ -123,7 +123,7 @@ async function testUnsupportedRelaysUpdateDomState() { const [eventFilter, uppercaseFilter, addressFilter] = filters; assert.deepEqual(eventFilter, { kinds: [COMMENT_EVENT_KIND], - "#e": ["unsupported-video"], + "#E": ["unsupported-video"], }); assert.deepEqual(uppercaseFilter, { kinds: [COMMENT_EVENT_KIND], @@ -133,7 +133,7 @@ async function testUnsupportedRelaysUpdateDomState() { }); assert.deepEqual(addressFilter, { kinds: [COMMENT_EVENT_KIND], - "#a": ["30078:pubkey-2:identifier-2"], + "#A": ["30078:pubkey-2:identifier-2"], }); return { total: 7, diff --git a/tests/nostr/comment-events.test.mjs b/tests/nostr/comment-events.test.mjs index 4131bca4..c111c535 100644 --- a/tests/nostr/comment-events.test.mjs +++ b/tests/nostr/comment-events.test.mjs @@ -118,13 +118,13 @@ 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)], - ["p", "thread-participant"], + ["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)], + ["P", "thread-participant"], ], - "comment event should include NIP-22 root tags while preserving fallback markers", + "comment event should include uppercase pointers and participant metadata", ); }); @@ -193,9 +193,9 @@ test("publishComment accepts legacy targets with only an event id", async () => [ ["E", "legacy-event-id"], ["K", String(COMMENT_EVENT_KIND)], - ["e", "legacy-event-id"], - ["e", "parent-legacy"], - ["k", String(COMMENT_EVENT_KIND)], + ["E", "legacy-event-id"], + ["E", "parent-legacy"], + ["K", String(COMMENT_EVENT_KIND)], ], "legacy publish should emit uppercase root metadata with lowercase fallbacks", ); @@ -213,10 +213,10 @@ test("publishComment derives root and parent metadata from parent comment tags", ["K", "30078"], ["P", "videopk", "wss://author"], ["A", "30078:videopk:root", "wss://definition"], - ["e", "video-event", "wss://root"], - ["e", "parent-comment", "wss://parent", "parentpk"], - ["k", String(COMMENT_EVENT_KIND)], - ["p", "parentpk", "wss://parent"], + ["E", "video-event", "wss://root"], + ["E", "parent-comment", "wss://parent", "parentpk"], + ["K", String(COMMENT_EVENT_KIND)], + ["P", "parentpk", "wss://parent"], ], }; @@ -240,11 +240,11 @@ 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)], - ["p", "parentpk", "wss://parent"], + ["E", "video-event", "wss://root"], + ["A", "30078:videopk:root", "wss://definition"], + ["E", "parent-comment", "wss://parent", "parentpk"], + ["K", String(COMMENT_EVENT_KIND)], + ["P", "parentpk", "wss://parent"], ]); }); @@ -255,11 +255,11 @@ test("listVideoComments matches comments even when tag casing and whitespace dif created_at: 1700000300, tags: [ [" A ", " 30078:AUTHOR:CLIP ", " WSS://Definition "], - ["e", " VIDEO-1 "], + ["E", " VIDEO-1 "], [" E ", " VIDEO-1 "], - ["e", " PARENT-1 "], - ["k", String(COMMENT_EVENT_KIND)], - ["p", " THREAD-PARTICIPANT "], + ["E", " PARENT-1 "], + ["K", String(COMMENT_EVENT_KIND)], + ["P", " THREAD-PARTICIPANT "], ], }; @@ -293,7 +293,7 @@ test("listVideoComments matches uppercase definition addresses without lowering" created_at: 1700000400, tags: [ ["A", uppercasePointer], - ["e", "video-1"], + ["E", "video-1"], ], }; @@ -324,11 +324,11 @@ test("listVideoComments matches comments even when tag casing and whitespace dif created_at: 1700000300, tags: [ [" A ", " 30078:AUTHOR:CLIP ", " WSS://Definition "], - ["e", " VIDEO-1 "], + ["E", " VIDEO-1 "], [" E ", " VIDEO-1 "], - ["e", " PARENT-1 "], - ["k", String(COMMENT_EVENT_KIND)], - ["p", " THREAD-PARTICIPANT "], + ["E", " PARENT-1 "], + ["K", String(COMMENT_EVENT_KIND)], + ["P", " THREAD-PARTICIPANT "], ], }; @@ -354,11 +354,11 @@ test("listVideoComments matches comments even when tag casing and whitespace dif ); }); - ["p", "parentpk", "wss://parent"], + ["P", "parentpk", "wss://parent"], ]); }); -test("publishComment emits only video event #e tag when address and parent are absent", async () => { +test("publishComment emits only uppercase video event pointer when address and parent are absent", async () => { const { client, publishCalls } = createMockClient(); const signer = { @@ -389,9 +389,8 @@ test("publishComment emits only video event #e tag when address and parent are a tags, [ ["E", "video-only"], - ["e", "video-only"], ], - "event should include an uppercase root pointer along with the legacy #e tag", + "event should include a single uppercase pointer", ); }); @@ -404,9 +403,9 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback ["A", "30078:author:clip"], ["K", "30078"], ["P", "author"], - ["e", "parent-1"], - ["k", String(COMMENT_EVENT_KIND)], - ["p", "thread-participant"], + ["E", "parent-1"], + ["K", String(COMMENT_EVENT_KIND)], + ["P", "thread-participant"], ], }; @@ -415,8 +414,8 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback kind: COMMENT_EVENT_KIND, created_at: 1690000000, tags: [ - ["a", "30078:author:clip"], - ["e", "parent-1"], + ["A", "30078:author:clip"], + ["E", "parent-1"], ], }; @@ -424,7 +423,7 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback id: "comment-3", kind: COMMENT_EVENT_KIND, created_at: 1700000100, - tags: [["a", "30078:author:clip"]], + tags: [["A", "30078:author:clip"]], }; const { @@ -473,9 +472,9 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback "event filter should target comment kind", ); assert.deepEqual( - eventFilter["#e"], + eventFilter["#E"], ["video-1"], - "event filter should target the video via #e", + "event filter should target the video via #E", ); assert.equal( uppercaseFilter.kinds[0], @@ -511,11 +510,11 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback parentFilter, { kinds: [COMMENT_EVENT_KIND], - "#e": ["parent-1"], + "#E": ["parent-1"], since: 1700000000, limit: 10, }, - "parent filter should scope the thread via #e", + "parent filter should scope the thread via #E", ); assert.equal( eventFilter.since, @@ -534,12 +533,12 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback "definition filter should target comment kind", ); assert.deepEqual( - definitionFilter["#a"], + definitionFilter["#A"], ["30078:author:clip"], - "definition filter should bind the video definition via #a", + "definition filter should bind the video definition via #A", ); assert.deepEqual( - definitionFilter["#e"], + definitionFilter["#E"], ["parent-1"], "definition filter should continue scoping to the parent comment", ); @@ -555,14 +554,14 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback ); }); -test("listVideoComments adds lowercase root filters when only the identifier is known", async () => { +test("listVideoComments emits uppercase root filters when only the identifier is known", async () => { const matchingEvent = { id: "comment-root", kind: COMMENT_EVENT_KIND, tags: [ ["I", "root-only"], ["P", "root-author"], - ["e", "video-rooted"], + ["E", "video-rooted"], ], }; @@ -589,44 +588,21 @@ test("listVideoComments adds lowercase root filters when only the identifier is assert.ok(receivedFilters, "pool.list should receive filters"); assert.equal( receivedFilters.filters.length, - 4, - "root identifier requests should include lowercase fallbacks", + 2, + "root identifier requests should emit uppercase filters only", ); - const [eventFilter, uppercaseEventFilter, lowercaseRootFilter, uppercaseRootFilter] = - receivedFilters.filters; + const [eventFilter, uppercaseRootFilter] = receivedFilters.filters; assert.deepEqual( eventFilter, - { - kinds: [COMMENT_EVENT_KIND], - "#e": ["video-rooted"], - since: 1700000000, - limit: 5, - }, - "event filter should continue targeting the video event id", - ); - - assert.deepEqual( - uppercaseEventFilter, { kinds: [COMMENT_EVENT_KIND], "#E": ["video-rooted"], since: 1700000000, limit: 5, }, - "uppercase event filter should continue targeting legacy #E pointers", - ); - - assert.deepEqual( - lowercaseRootFilter, - { - kinds: [COMMENT_EVENT_KIND], - "#i": ["root-only"], - since: 1700000000, - limit: 5, - }, - "root identifier filter should include lowercase #i tag queries", + "event filter should target the video event id via #E", ); assert.equal( @@ -660,7 +636,7 @@ test("listVideoComments supports legacy targets without a definition address", a const legacyEvent = { id: "legacy-comment", kind: COMMENT_EVENT_KIND, - tags: [["e", "legacy-video"]], + tags: [["E", "legacy-video"]], }; const { client, pool } = createMockClient(); @@ -688,14 +664,14 @@ test("listVideoComments supports legacy targets without a definition address", a [ { kinds: [COMMENT_EVENT_KIND], - "#e": ["legacy-video"], + "#E": ["legacy-video"], }, { kinds: [COMMENT_EVENT_KIND], "#E": ["legacy-video"], }, ], - "legacy listing should target the video event id via #e/#E", + "legacy listing should target the video event id via #E", ); }); @@ -756,7 +732,7 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe" [ { kinds: [COMMENT_EVENT_KIND], - "#e": ["video-1"], + "#E": ["video-1"], }, { kinds: [COMMENT_EVENT_KIND], @@ -766,12 +742,12 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe" }, { kinds: [COMMENT_EVENT_KIND], - "#e": ["parent-1"], + "#E": ["parent-1"], }, { kinds: [COMMENT_EVENT_KIND], "#a": ["30078:author:clip"], - "#e": ["parent-1"], + "#E": ["parent-1"], }, ], "subscription should emit event, parent, and address filters", @@ -784,16 +760,16 @@ test("subscribeVideoComments forwards matching events and cleans up unsubscribe" ["A", "30078:author:clip"], ["K", "30078"], ["P", "author"], - ["e", "parent-1"], - ["k", String(COMMENT_EVENT_KIND)], - ["p", "thread-participant"], + ["E", "parent-1"], + ["K", String(COMMENT_EVENT_KIND)], + ["P", "thread-participant"], ], }); handler({ id: "comment-rejected", kind: COMMENT_EVENT_KIND, - tags: [["e", "video-1"]], + tags: [["E", "video-1"]], }); assert.equal(receivedEvents.length, 1, "only matching events should reach the callback"); @@ -839,7 +815,7 @@ test("subscribeVideoComments supports video targets without a definition address [ { kinds: [COMMENT_EVENT_KIND], - "#e": ["legacy-video"], + "#E": ["legacy-video"], }, { kinds: [COMMENT_EVENT_KIND], @@ -854,7 +830,7 @@ test("subscribeVideoComments supports video targets without a definition address handler({ id: "legacy-comment", kind: COMMENT_EVENT_KIND, - tags: [["e", "legacy-video"]], + tags: [["E", "legacy-video"]], }); assert.equal(receivedEvents.length, 1, "legacy comment should be forwarded");