From 2dd2be9345433b340e3c89cb9f521008fb48baa7 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 12 Dec 2025 11:05:00 -0500 Subject: [PATCH] Normalize comment thread event ids --- js/services/commentThreadService.js | 25 ++++--- tests/comment-thread-service.test.mjs | 100 ++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/js/services/commentThreadService.js b/js/services/commentThreadService.js index bc5e4057..0754fdc7 100644 --- a/js/services/commentThreadService.js +++ b/js/services/commentThreadService.js @@ -24,6 +24,11 @@ function normalizeString(value) { return typeof value === "string" ? value.trim() : ""; } +function normalizeHexId(value) { + const normalized = normalizeString(value); + return normalized ? normalized.toLowerCase() : ""; +} + function normalizeRelay(value) { if (typeof value === "string") { return value.trim(); @@ -181,7 +186,7 @@ export default class CommentThreadService { const rawVideoAuthorPubkey = normalizeString(video?.pubkey); - this.videoEventId = normalizeString(video?.id); + this.videoEventId = normalizeHexId(video?.id); this.videoAddressPointer = buildVideoAddressPointer(video); this.videoKind = normalizeKind(video?.kind); this.videoAuthorPubkeyRaw = rawVideoAuthorPubkey; @@ -194,7 +199,7 @@ export default class CommentThreadService { this.videoRootRelay = normalizeRelay( video?.videoRootRelay || video?.rootIdentifierRelay, ); - this.parentCommentId = normalizeString(parentCommentId); + this.parentCommentId = normalizeHexId(parentCommentId); this.parentCommentKind = this.parentCommentId ? String(COMMENT_EVENT_KIND) : ""; this.parentCommentPubkey = ""; this.activeRelays = Array.isArray(relays) ? [...relays] : null; @@ -290,7 +295,7 @@ export default class CommentThreadService { const targetCandidate = target && typeof target === "object" ? target : {}; - const videoEventId = normalizeString( + const videoEventId = normalizeHexId( targetCandidate.videoEventId || targetCandidate.eventId, ); @@ -323,7 +328,7 @@ export default class CommentThreadService { } getCommentCacheKey(videoEventId) { - const normalized = normalizeString(videoEventId); + const normalized = normalizeHexId(videoEventId); if (!normalized) { return ""; } @@ -507,8 +512,8 @@ export default class CommentThreadService { return aTime - bTime; } - const aId = normalizeString(a?.id); - const bId = normalizeString(b?.id); + const aId = normalizeHexId(a?.id); + const bId = normalizeHexId(b?.id); if (aId && bId) { return aId.localeCompare(bId); } @@ -576,7 +581,7 @@ export default class CommentThreadService { return null; } - const eventId = normalizeString(event.id); + const eventId = normalizeHexId(event.id); if (!eventId) { return null; } @@ -640,7 +645,7 @@ export default class CommentThreadService { if (name !== "e") { continue; } - const normalizedValue = normalizeString(value); + const normalizedValue = normalizeHexId(value); if (!normalizedValue) { continue; } @@ -929,13 +934,13 @@ export default class CommentThreadService { } getCommentIdsForParent(parentId) { - const key = toParentKey(normalizeString(parentId)); + const key = toParentKey(normalizeHexId(parentId)); const list = this.childrenByParent.get(key); return Array.isArray(list) ? [...list] : []; } getCommentEvent(commentId) { - const normalized = normalizeString(commentId); + const normalized = normalizeHexId(commentId); if (!normalized) { return null; } diff --git a/tests/comment-thread-service.test.mjs b/tests/comment-thread-service.test.mjs index 63cf3022..199321eb 100644 --- a/tests/comment-thread-service.test.mjs +++ b/tests/comment-thread-service.test.mjs @@ -123,6 +123,106 @@ test("CommentThreadService caches mixed-case video ids consistently", () => { ); }); +test("CommentThreadService normalizes mixed-case event ids in thread state", async () => { + const video = { ...createBaseVideo(), id: "VideoMixed123" }; + const topLevelId = "CoMmEnT-Root"; + const replyId = "RePlY-Child"; + + const topLevelEvent = { + id: topLevelId, + kind: 1, + pubkey: "commenterpk", + created_at: 1700000000, + content: "Top level", + tags: [ + ["e", video.id], + ["a", "30078:authorpk:video-root"], + ], + }; + + const replyEvent = { + id: replyId, + kind: 1, + pubkey: "replypk", + created_at: 1700000100, + content: "Reply", + tags: [ + ["e", video.id.toUpperCase()], + ["a", "30078:authorpk:video-root"], + ["e", topLevelId.toUpperCase()], + ], + }; + + const fetchVideoComments = async () => [topLevelEvent]; + let triggerAppend = null; + const subscribeVideoComments = (target, options) => { + triggerAppend = options?.onEvent; + return () => {}; + }; + + const service = new CommentThreadService({ + fetchVideoComments, + subscribeVideoComments, + }); + + let threadReadyPayload = null; + let appendPayload = null; + service.setCallbacks({ + onThreadReady: (payload) => { + threadReadyPayload = payload; + }, + onCommentsAppended: (payload) => { + appendPayload = payload; + }, + }); + + await service.loadThread({ video }); + + assert.equal( + threadReadyPayload.videoEventId, + video.id.toLowerCase(), + "video ids should normalize to lowercase in the thread payload", + ); + assert.deepEqual( + threadReadyPayload.topLevelIds, + [topLevelId.toLowerCase()], + "top-level ids should be normalized", + ); + assert.equal( + threadReadyPayload.childrenByParent.get(null)[0], + topLevelId.toLowerCase(), + "root children should reference normalized ids", + ); + assert.equal( + threadReadyPayload.commentsById.has(topLevelId.toLowerCase()), + true, + "comment map should key by normalized ids", + ); + assert.ok( + service.getCommentEvent(topLevelId.toUpperCase()), + "comment lookups should be case-insensitive", + ); + + assert.equal(typeof triggerAppend, "function", "subscription should be active"); + triggerAppend(replyEvent); + + assert.deepEqual( + appendPayload.commentIds, + [replyId.toLowerCase()], + "append payload should emit normalized comment ids", + ); + assert.equal( + appendPayload.parentCommentId, + topLevelId.toLowerCase(), + "append payload should carry normalized parent ids", + ); + assert.deepEqual( + appendPayload.childrenByParent.get(topLevelId.toLowerCase()), + [replyId.toLowerCase()], + "children map should store normalized ids for replies", + ); +}); + test( "listVideoComments accepts builder events without parent ids and filters replies", async () => {