Merge pull request #995 from PR0M3TH3AN/codex/enhance-comment-metadata-handling

Improve comment metadata propagation
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-29 15:24:24 -04:00
commit a29db655c0
6 files changed
+655 -42

No files matched your search

+350 -42
View File
@@ -63,8 +63,10 @@ function normalizePointerCandidate(candidate, expectedType) {
}
if (Array.isArray(candidate)) {
const tagName =
typeof candidate[0] === "string" ? candidate[0].trim().toLowerCase() : "";
if (
candidate[0] === expectedType &&
tagName === expectedType &&
typeof candidate[1] === "string" &&
candidate[1].trim()
) {
@@ -96,7 +98,7 @@ function normalizePointerCandidate(candidate, expectedType) {
if (candidate && typeof candidate === "object") {
if (
typeof candidate.type === "string" &&
candidate.type === expectedType &&
candidate.type.toLowerCase() === expectedType &&
typeof candidate.value === "string" &&
candidate.value.trim()
) {
@@ -145,6 +147,74 @@ function pickString(...candidates) {
return "";
}
function pickKind(...candidates) {
for (const candidate of candidates) {
if (Number.isFinite(candidate)) {
return String(Math.floor(candidate));
}
if (typeof candidate === "string" && candidate.trim()) {
return candidate.trim();
}
}
return "";
}
function isEventCandidate(candidate) {
if (!candidate || typeof candidate !== "object") {
return false;
}
if (Array.isArray(candidate.tags)) {
return true;
}
if (typeof candidate.id === "string" && candidate.id.trim()) {
return true;
}
if (typeof candidate.pubkey === "string" && candidate.pubkey.trim()) {
return true;
}
return false;
}
function resolveEventCandidate(...candidates) {
for (const candidate of candidates) {
if (isEventCandidate(candidate)) {
return candidate;
}
}
return null;
}
function collectTagsFromEvent(event) {
if (!event || typeof event !== "object") {
return [];
}
const tags = Array.isArray(event.tags) ? event.tags : [];
return tags.filter((tag) => Array.isArray(tag) && tag.length >= 2);
}
function findTagByName(tags, ...names) {
const normalizedNames = names
.flat()
.map((name) => (typeof name === "string" ? name.trim() : ""))
.filter(Boolean);
if (!normalizedNames.length) {
return null;
}
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
const [name] = tag;
if (typeof name !== "string") {
continue;
}
if (normalizedNames.includes(name.trim())) {
return tag;
}
}
return null;
}
function normalizeCommentTarget(targetInput = {}, overrides = {}) {
const target = targetInput && typeof targetInput === "object" ? targetInput : {};
const options = overrides && typeof overrides === "object" ? overrides : {};
@@ -165,6 +235,18 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
normalizePointerCandidate(target.definitionPointer, "a") ||
normalizePointerCandidate(target.definition, "a");
const rootDefinitionPointer =
normalizePointerCandidate(options.rootDefinitionPointer, "a") ||
normalizePointerCandidate(options.rootPointer, "a") ||
normalizePointerCandidate(target.rootDefinitionPointer, "a") ||
normalizePointerCandidate(target.rootPointer, "a");
const rootEventPointer =
normalizePointerCandidate(options.rootEventPointer, "e") ||
normalizePointerCandidate(options.rootPointer, "e") ||
normalizePointerCandidate(target.rootEventPointer, "e") ||
normalizePointerCandidate(target.rootPointer, "e");
const parentCommentPointer =
normalizePointerCandidate(options.parentCommentPointer, "e") ||
normalizePointerCandidate(options.parentComment, "e") ||
@@ -172,17 +254,41 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
normalizePointerCandidate(target.parentComment, "e") ||
normalizePointerCandidate(target.parentPointer, "e");
const videoEventObject = resolveEventCandidate(
options.videoEvent,
options.rootEvent,
target.videoEvent,
target.rootEvent,
);
const parentCommentEvent = resolveEventCandidate(
options.parentCommentEvent,
options.parentComment,
target.parentCommentEvent,
target.parentComment,
);
const rootEventTags = collectTagsFromEvent(videoEventObject);
const parentEventTags = collectTagsFromEvent(parentCommentEvent);
const combinedTags = [...parentEventTags, ...rootEventTags];
const getTagField = (tag, index) =>
Array.isArray(tag) && typeof tag[index] === "string"
? tag[index].trim()
: "";
const videoEventId = pickString(
options.videoEventId,
target.videoEventId,
target.eventId,
videoEventPointer?.value,
rootEventPointer?.value,
);
const videoEventRelay = pickString(
options.videoEventRelay,
target.videoEventRelay,
target.eventRelay,
videoEventPointer?.relay,
rootEventPointer?.relay,
);
const videoDefinitionAddress = pickString(
@@ -190,12 +296,14 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
target.videoDefinitionAddress,
target.definitionAddress,
videoDefinitionPointer?.value,
rootDefinitionPointer?.value,
);
const videoDefinitionRelay = pickString(
options.videoDefinitionRelay,
target.videoDefinitionRelay,
target.definitionRelay,
videoDefinitionPointer?.relay,
rootDefinitionPointer?.relay,
);
const parentCommentId = pickString(
@@ -223,17 +331,17 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
target.participantRelay,
);
const normalizedVideoEventId =
let normalizedVideoEventId =
typeof videoEventId === "string" ? videoEventId.trim() : "";
const normalizedVideoDefinitionAddress =
let normalizedVideoDefinitionAddress =
typeof videoDefinitionAddress === "string" ? videoDefinitionAddress.trim() : "";
const normalizedVideoEventRelay =
let normalizedVideoEventRelay =
typeof videoEventRelay === "string" ? videoEventRelay.trim() : "";
const normalizedVideoDefinitionRelay =
let normalizedVideoDefinitionRelay =
typeof videoDefinitionRelay === "string" ? videoDefinitionRelay.trim() : "";
const normalizedParentCommentId =
let normalizedParentCommentId =
typeof parentCommentId === "string" ? parentCommentId.trim() : "";
const normalizedParentCommentRelay =
let normalizedParentCommentRelay =
typeof parentCommentRelay === "string" ? parentCommentRelay.trim() : "";
const normalizedThreadParticipantPubkey =
typeof threadParticipantPubkey === "string" ? threadParticipantPubkey.trim() : "";
@@ -264,15 +372,27 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
const normalizedRootIdentifier =
typeof rootIdentifier === "string" ? rootIdentifier.trim() : "";
const normalizedRootIdentifierRelay =
let normalizedRootIdentifierRelay =
typeof rootIdentifierRelay === "string" ? rootIdentifierRelay.trim() : "";
const normalizedParentIdentifier =
typeof parentIdentifier === "string" ? parentIdentifier.trim() : "";
const normalizedParentIdentifierRelay =
let normalizedParentIdentifierRelay =
typeof parentIdentifierRelay === "string"
? parentIdentifierRelay.trim()
: "";
let normalizedVideoKind = pickKind(
options.videoKind,
target.videoKind,
videoEventObject?.kind,
);
let normalizedVideoAuthorPubkey = pickString(
options.videoAuthorPubkey,
target.videoAuthorPubkey,
videoEventObject?.pubkey,
);
const definitionSegments = normalizedVideoDefinitionAddress
? normalizedVideoDefinitionAddress.split(":")
: [];
@@ -284,6 +404,7 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
target.rootKind,
options.videoKind,
target.videoKind,
normalizedVideoKind,
derivedDefinitionKind,
);
let normalizedRootKind =
@@ -292,10 +413,227 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
const parentKindCandidate = pickString(
options.parentKind,
target.parentKind,
options.parentCommentKind,
target.parentCommentKind,
);
let normalizedParentKind =
typeof parentKindCandidate === "string" ? parentKindCandidate.trim() : "";
let normalizedRootAuthorPubkey = pickString(
options.rootAuthorPubkey,
target.rootAuthorPubkey,
options.videoAuthorPubkey,
target.videoAuthorPubkey,
normalizedVideoAuthorPubkey,
derivedDefinitionPubkey,
);
normalizedRootAuthorPubkey =
typeof normalizedRootAuthorPubkey === "string"
? normalizedRootAuthorPubkey.trim()
: "";
let normalizedParentAuthorPubkey = pickString(
options.parentAuthorPubkey,
target.parentAuthorPubkey,
options.parentCommentPubkey,
target.parentCommentPubkey,
);
normalizedParentAuthorPubkey =
typeof normalizedParentAuthorPubkey === "string"
? normalizedParentAuthorPubkey.trim()
: "";
let normalizedRootAuthorRelay = pickString(
options.rootAuthorRelay,
target.rootAuthorRelay,
);
normalizedRootAuthorRelay =
typeof normalizedRootAuthorRelay === "string"
? normalizedRootAuthorRelay.trim()
: "";
let normalizedParentAuthorRelay = pickString(
options.parentAuthorRelay,
target.parentAuthorRelay,
);
normalizedParentAuthorRelay =
typeof normalizedParentAuthorRelay === "string"
? normalizedParentAuthorRelay.trim()
: "";
if (!normalizedRootIdentifier) {
const tag = findTagByName(combinedTags, "I");
const value = getTagField(tag, 1);
if (value) {
normalizedRootIdentifier = value;
const relay = getTagField(tag, 2);
if (relay) {
normalizedRootIdentifierRelay = relay;
}
}
} else if (!normalizedRootIdentifierRelay) {
const tag = findTagByName(combinedTags, "I");
const relay = getTagField(tag, 2);
if (relay) {
normalizedRootIdentifierRelay = relay;
}
}
if (!normalizedVideoDefinitionAddress) {
const tag = findTagByName(combinedTags, "A", "a");
const value = getTagField(tag, 1);
if (value) {
normalizedVideoDefinitionAddress = value;
const relay = getTagField(tag, 2);
if (relay) {
normalizedVideoDefinitionRelay = relay;
}
}
} else if (!normalizedVideoDefinitionRelay) {
const tag = findTagByName(combinedTags, "A", "a");
const relay = getTagField(tag, 2);
if (relay) {
normalizedVideoDefinitionRelay = relay;
}
}
const rootEventTag = findTagByName(combinedTags, "E");
if (rootEventTag) {
const value = getTagField(rootEventTag, 1);
if (value && !normalizedVideoEventId) {
normalizedVideoEventId = value;
}
const relay = getTagField(rootEventTag, 2);
if (relay && !normalizedVideoEventRelay) {
normalizedVideoEventRelay = relay;
}
const authorHint = getTagField(rootEventTag, 3);
if (authorHint && !normalizedRootAuthorPubkey) {
normalizedRootAuthorPubkey = authorHint;
}
}
if (!normalizedVideoEventId || !normalizedParentCommentId) {
const eventValues = [];
for (const tag of combinedTags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
if (typeof tag[0] === "string" && tag[0].trim().toLowerCase() === "e") {
const value = getTagField(tag, 1);
if (value) {
eventValues.push({ tag, value });
}
}
}
if (!normalizedVideoEventId && eventValues.length) {
normalizedVideoEventId = eventValues[0].value;
const relay = getTagField(eventValues[0].tag, 2);
if (relay && !normalizedVideoEventRelay) {
normalizedVideoEventRelay = relay;
}
}
if (!normalizedParentCommentId && eventValues.length) {
for (let index = eventValues.length - 1; index >= 0; index -= 1) {
const candidate = eventValues[index];
if (
candidate.value &&
candidate.value !== normalizedVideoEventId
) {
normalizedParentCommentId = candidate.value;
const relay = getTagField(candidate.tag, 2);
if (relay) {
normalizedParentCommentRelay = relay;
}
const authorHint = getTagField(candidate.tag, 3);
if (authorHint && !normalizedParentAuthorPubkey) {
normalizedParentAuthorPubkey = authorHint;
}
break;
}
}
}
}
if (normalizedParentCommentId && !normalizedParentCommentRelay) {
const parentTag = parentEventTags.find(
(tag) =>
Array.isArray(tag) &&
tag.length >= 2 &&
typeof tag[0] === "string" &&
tag[0].trim().toLowerCase() === "e" &&
getTagField(tag, 1) === normalizedParentCommentId,
);
if (parentTag) {
const relay = getTagField(parentTag, 2);
if (relay) {
normalizedParentCommentRelay = relay;
}
const authorHint = getTagField(parentTag, 3);
if (authorHint && !normalizedParentAuthorPubkey) {
normalizedParentAuthorPubkey = authorHint;
}
}
}
if (!normalizedParentIdentifierRelay) {
const tag = findTagByName(parentEventTags, "i");
const relay = getTagField(tag, 2);
if (relay) {
normalizedParentIdentifierRelay = relay;
}
}
if (!normalizedRootKind) {
const tag = findTagByName(combinedTags, "K");
const value = getTagField(tag, 1);
if (value) {
normalizedRootKind = value;
}
}
if (!normalizedVideoKind && normalizedRootKind) {
normalizedVideoKind = normalizedRootKind;
}
if (!normalizedRootAuthorPubkey || !normalizedRootAuthorRelay) {
const tag = findTagByName(combinedTags, "P");
if (tag) {
const value = getTagField(tag, 1);
if (value && !normalizedRootAuthorPubkey) {
normalizedRootAuthorPubkey = value;
}
const relay = getTagField(tag, 2);
if (relay && !normalizedRootAuthorRelay) {
normalizedRootAuthorRelay = relay;
}
}
}
if (!normalizedVideoAuthorPubkey && normalizedRootAuthorPubkey) {
normalizedVideoAuthorPubkey = normalizedRootAuthorPubkey;
}
if (!normalizedParentKind) {
const tag = findTagByName(parentEventTags, "k");
const value = getTagField(tag, 1);
if (value) {
normalizedParentKind = value;
}
}
if (!normalizedParentAuthorPubkey || !normalizedParentAuthorRelay) {
const tag = findTagByName(parentEventTags, "p");
if (tag) {
const value = getTagField(tag, 1);
if (value && !normalizedParentAuthorPubkey) {
normalizedParentAuthorPubkey = value;
}
const relay = getTagField(tag, 2);
if (relay && !normalizedParentAuthorRelay) {
normalizedParentAuthorRelay = relay;
}
}
}
if (!normalizedParentKind) {
if (normalizedParentCommentId) {
normalizedParentKind = String(COMMENT_EVENT_KIND);
@@ -310,22 +648,6 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
normalizedRootKind = normalizedParentKind;
}
const rootAuthorCandidate = pickString(
options.rootAuthorPubkey,
target.rootAuthorPubkey,
derivedDefinitionPubkey,
);
let normalizedRootAuthorPubkey =
typeof rootAuthorCandidate === "string" ? rootAuthorCandidate.trim() : "";
const parentAuthorCandidate = pickString(
options.parentAuthorPubkey,
target.parentAuthorPubkey,
);
let normalizedParentAuthorPubkey =
typeof parentAuthorCandidate === "string"
? parentAuthorCandidate.trim()
: "";
if (!normalizedParentAuthorPubkey) {
normalizedParentAuthorPubkey = normalizedThreadParticipantPubkey;
}
@@ -336,26 +658,10 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
}
}
const rootAuthorRelayCandidate = pickString(
options.rootAuthorRelay,
target.rootAuthorRelay,
);
let normalizedRootAuthorRelay =
typeof rootAuthorRelayCandidate === "string"
? rootAuthorRelayCandidate.trim()
: "";
if (!normalizedRootAuthorRelay && normalizedVideoDefinitionRelay) {
normalizedRootAuthorRelay = normalizedVideoDefinitionRelay;
}
const parentAuthorRelayCandidate = pickString(
options.parentAuthorRelay,
target.parentAuthorRelay,
);
let normalizedParentAuthorRelay =
typeof parentAuthorRelayCandidate === "string"
? parentAuthorRelayCandidate.trim()
: "";
if (!normalizedParentAuthorRelay) {
normalizedParentAuthorRelay = normalizedThreadParticipantRelay;
}
@@ -385,6 +691,8 @@ function normalizeCommentTarget(targetInput = {}, overrides = {}) {
videoEventRelay: normalizedVideoEventRelay,
videoDefinitionAddress: normalizedVideoDefinitionAddress,
videoDefinitionRelay: normalizedVideoDefinitionRelay,
videoKind: normalizedVideoKind,
videoAuthorPubkey: normalizedVideoAuthorPubkey,
parentCommentId: normalizedParentCommentId,
parentCommentRelay: normalizedParentCommentRelay,
threadParticipantPubkey: normalizedThreadParticipantPubkey,
+65
View File
@@ -2,6 +2,7 @@
import { devLogger } from "../utils/logger.js";
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
import { COMMENT_EVENT_KIND } from "../nostr/commentEvents.js";
const ROOT_PARENT_KEY = "__root__";
const DEFAULT_INITIAL_LIMIT = 40;
@@ -24,6 +25,16 @@ function normalizePubkey(pubkey) {
return normalized ? normalized.toLowerCase() : "";
}
function normalizeKind(value) {
if (Number.isFinite(value)) {
return String(Math.floor(value));
}
if (typeof value === "string" && value.trim()) {
return value.trim();
}
return "";
}
function toParentKey(parentId) {
return parentId ? parentId : ROOT_PARENT_KEY;
}
@@ -112,7 +123,11 @@ export default class CommentThreadService {
this.subscriptionCleanup = null;
this.videoEventId = "";
this.videoAddressPointer = "";
this.videoKind = "";
this.videoAuthorPubkey = "";
this.parentCommentId = "";
this.parentCommentKind = "";
this.parentCommentPubkey = "";
this.activeRelays = null;
}
@@ -138,7 +153,11 @@ export default class CommentThreadService {
this.videoEventId = normalizeString(video?.id);
this.videoAddressPointer = buildVideoAddressPointer(video);
this.videoKind = normalizeKind(video?.kind);
this.videoAuthorPubkey = normalizePubkey(video?.pubkey);
this.parentCommentId = normalizeString(parentCommentId);
this.parentCommentKind = this.parentCommentId ? String(COMMENT_EVENT_KIND) : "";
this.parentCommentPubkey = "";
this.activeRelays = Array.isArray(relays) ? [...relays] : null;
if (!this.videoEventId) {
@@ -164,6 +183,20 @@ export default class CommentThreadService {
if (this.videoAddressPointer) {
target.videoDefinitionAddress = this.videoAddressPointer;
}
if (this.videoKind) {
target.videoKind = this.videoKind;
}
if (this.videoAuthorPubkey) {
target.videoAuthorPubkey = this.videoAuthorPubkey;
}
if (this.parentCommentId) {
if (this.parentCommentKind) {
target.parentCommentKind = this.parentCommentKind;
}
if (this.parentCommentPubkey) {
target.parentCommentPubkey = this.parentCommentPubkey;
}
}
let events = [];
try {
@@ -255,6 +288,15 @@ export default class CommentThreadService {
const parentKey = toParentKey(parentId);
const createdAt = Number.isFinite(event.created_at) ? event.created_at : 0;
const pubkey = normalizePubkey(event.pubkey);
if (this.parentCommentId && eventId === this.parentCommentId) {
const kindValue = normalizeKind(event.kind);
if (kindValue) {
this.parentCommentKind = kindValue;
}
if (pubkey) {
this.parentCommentPubkey = pubkey;
}
}
const existingMeta = this.metaById.get(eventId);
this.eventsById.set(eventId, event);
@@ -458,6 +500,11 @@ export default class CommentThreadService {
const payload = {
videoEventId: this.videoEventId,
parentCommentId: this.parentCommentId || null,
videoDefinitionAddress: this.videoAddressPointer || null,
videoKind: this.videoKind || null,
videoAuthorPubkey: this.videoAuthorPubkey || null,
parentCommentKind: this.parentCommentKind || null,
parentCommentPubkey: this.parentCommentPubkey || null,
topLevelIds: this.getCommentIdsForParent(null),
commentsById: this.cloneCommentsMap(),
childrenByParent: this.cloneTreeMap(),
@@ -470,6 +517,11 @@ export default class CommentThreadService {
const payload = {
videoEventId: this.videoEventId,
parentCommentId: parentId || null,
videoDefinitionAddress: this.videoAddressPointer || null,
videoKind: this.videoKind || null,
videoAuthorPubkey: this.videoAuthorPubkey || null,
parentCommentKind: this.parentCommentKind || null,
parentCommentPubkey: this.parentCommentPubkey || null,
commentIds: [eventId],
commentsById: this.cloneCommentsMap(),
childrenByParent: this.cloneTreeMap(),
@@ -489,6 +541,11 @@ export default class CommentThreadService {
return {
videoEventId: this.videoEventId,
parentCommentId: this.parentCommentId || null,
videoDefinitionAddress: this.videoAddressPointer || null,
videoKind: this.videoKind || null,
videoAuthorPubkey: this.videoAuthorPubkey || null,
parentCommentKind: this.parentCommentKind || null,
parentCommentPubkey: this.parentCommentPubkey || null,
commentsById: this.cloneCommentsMap(),
childrenByParent: this.cloneTreeMap(),
profiles: this.getProfilesSnapshot(),
@@ -536,6 +593,14 @@ export default class CommentThreadService {
return Array.isArray(list) ? [...list] : [];
}
getCommentEvent(commentId) {
const normalized = normalizeString(commentId);
if (!normalized) {
return null;
}
return this.eventsById.get(normalized) || null;
}
waitForProfileHydration() {
return this.profileHydrationPromise || Promise.resolve();
}
+107
View File
@@ -1,5 +1,6 @@
import { devLogger } from "../utils/logger.js";
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
import { COMMENT_EVENT_KIND } from "../nostr/commentEvents.js";
const DEFAULT_LIMIT = 40;
@@ -7,6 +8,16 @@ function normalizeString(value) {
return typeof value === "string" ? value.trim() : "";
}
function normalizeKind(value) {
if (Number.isFinite(value)) {
return String(Math.floor(value));
}
if (typeof value === "string" && value.trim()) {
return value.trim();
}
return "";
}
export default class VideoModalCommentController {
constructor({
commentThreadService = null,
@@ -73,7 +84,11 @@ export default class VideoModalCommentController {
this.modalCommentState = {
videoEventId: null,
videoDefinitionAddress: null,
videoKind: null,
videoAuthorPubkey: null,
parentCommentId: null,
parentCommentKind: null,
parentCommentPubkey: null,
};
this.modalCommentProfiles = new Map();
this.modalCommentLimit = this.commentThreadService?.defaultLimit || DEFAULT_LIMIT;
@@ -131,6 +146,8 @@ export default class VideoModalCommentController {
const videoEventId = normalizeString(video.id);
const videoDefinitionAddress = buildVideoAddressPointer(video);
const videoKind = normalizeKind(video.kind);
const videoAuthorPubkey = normalizeString(video.pubkey);
if (!videoEventId) {
this.resetModalCommentState({ hide: false });
@@ -143,7 +160,11 @@ export default class VideoModalCommentController {
this.modalCommentState = {
videoEventId,
videoDefinitionAddress: videoDefinitionAddress || null,
videoKind: videoKind || null,
videoAuthorPubkey: videoAuthorPubkey || null,
parentCommentId: null,
parentCommentKind: null,
parentCommentPubkey: null,
};
this.modalCommentProfiles = new Map();
if (this.commentThreadService?.defaultLimit) {
@@ -221,7 +242,11 @@ export default class VideoModalCommentController {
this.modalCommentState = {
videoEventId: null,
videoDefinitionAddress: null,
videoKind: null,
videoAuthorPubkey: null,
parentCommentId: null,
parentCommentKind: null,
parentCommentPubkey: null,
};
this.modalCommentProfiles = new Map();
if (resetUi) {
@@ -311,6 +336,28 @@ export default class VideoModalCommentController {
this.modalCommentState.parentCommentId = normalizeString(
snapshot.parentCommentId,
) || null;
if (typeof snapshot.videoDefinitionAddress === "string") {
const pointer = normalizeString(snapshot.videoDefinitionAddress);
if (pointer) {
this.modalCommentState.videoDefinitionAddress = pointer;
}
}
const snapshotVideoKind = normalizeKind(snapshot.videoKind);
if (snapshotVideoKind) {
this.modalCommentState.videoKind = snapshotVideoKind;
}
const snapshotVideoAuthor = normalizeString(snapshot.videoAuthorPubkey);
if (snapshotVideoAuthor) {
this.modalCommentState.videoAuthorPubkey = snapshotVideoAuthor;
}
const snapshotParentKind = normalizeKind(snapshot.parentCommentKind);
if (snapshotParentKind) {
this.modalCommentState.parentCommentKind = snapshotParentKind;
}
const snapshotParentPubkey = normalizeString(snapshot.parentCommentPubkey);
if (snapshotParentPubkey) {
this.modalCommentState.parentCommentPubkey = snapshotParentPubkey;
}
const sanitizedSnapshot = this.buildModalCommentSnapshot(snapshot);
this.videoModal.renderComments?.(sanitizedSnapshot);
@@ -346,6 +393,19 @@ export default class VideoModalCommentController {
return;
}
const enriched = this.enrichCommentEvent(event);
if (
this.modalCommentState.parentCommentId &&
commentId === this.modalCommentState.parentCommentId
) {
const eventKind = normalizeKind(event.kind);
if (eventKind) {
this.modalCommentState.parentCommentKind = eventKind;
}
const eventPubkey = normalizeString(event.pubkey);
if (eventPubkey) {
this.modalCommentState.parentCommentPubkey = eventPubkey;
}
}
this.videoModal.appendComment?.(enriched);
});
}
@@ -486,6 +546,10 @@ export default class VideoModalCommentController {
const videoEventId = normalizeString(video.id);
const videoDefinitionAddress = buildVideoAddressPointer(video);
const videoKind =
this.modalCommentState.videoKind || normalizeKind(video.kind);
const videoAuthorPubkey =
this.modalCommentState.videoAuthorPubkey || normalizeString(video.pubkey);
if (!videoEventId) {
this.callbacks.showError("Comments are unavailable for this video.");
@@ -493,6 +557,34 @@ export default class VideoModalCommentController {
}
const parentCommentId = normalizeString(detail.parentId) || null;
let parentCommentKind = this.modalCommentState.parentCommentKind
? normalizeKind(this.modalCommentState.parentCommentKind)
: "";
let parentCommentPubkey = this.modalCommentState.parentCommentPubkey
? normalizeString(this.modalCommentState.parentCommentPubkey)
: "";
if (parentCommentId && typeof this.commentThreadService?.getCommentEvent === "function") {
const parentEvent = this.commentThreadService.getCommentEvent(
parentCommentId,
);
if (parentEvent) {
const eventKind = normalizeKind(parentEvent.kind);
if (eventKind) {
parentCommentKind = eventKind;
this.modalCommentState.parentCommentKind = eventKind;
}
const eventPubkey = normalizeString(parentEvent.pubkey);
if (eventPubkey) {
parentCommentPubkey = eventPubkey;
this.modalCommentState.parentCommentPubkey = eventPubkey;
}
}
}
if (!parentCommentKind && parentCommentId) {
parentCommentKind = String(COMMENT_EVENT_KIND);
}
this.videoModal.setCommentComposerState?.({
disabled: true,
@@ -507,6 +599,21 @@ export default class VideoModalCommentController {
if (videoDefinitionAddress) {
publishPayload.videoDefinitionAddress = videoDefinitionAddress;
}
if (videoKind) {
publishPayload.videoKind = videoKind;
publishPayload.rootKind = videoKind;
}
if (videoAuthorPubkey) {
publishPayload.videoAuthorPubkey = videoAuthorPubkey;
publishPayload.rootAuthorPubkey = videoAuthorPubkey;
}
if (parentCommentKind) {
publishPayload.parentCommentKind = parentCommentKind;
}
if (parentCommentPubkey) {
publishPayload.parentCommentPubkey = parentCommentPubkey;
publishPayload.parentAuthorPubkey = parentCommentPubkey;
}
const publishPromise = Promise.resolve(
this.services.publishComment(
+12
View File
@@ -113,6 +113,8 @@ test(
assert.deepStrictEqual(lastFetchTarget, {
videoEventId: "video123",
videoDefinitionAddress: "30078:authorpk:video-root",
videoKind: "30078",
videoAuthorPubkey: "authorpk",
parentCommentId: "",
});
assert.deepStrictEqual(lastFetchOptions, {
@@ -129,6 +131,11 @@ test(
const initialSnapshot = initialPayloads[0];
assert.ok(initialSnapshot.childrenByParent instanceof Map);
assert.equal(initialSnapshot.videoDefinitionAddress, "30078:authorpk:video-root");
assert.equal(initialSnapshot.videoKind, "30078");
assert.equal(initialSnapshot.videoAuthorPubkey, "authorpk");
assert.equal(initialSnapshot.parentCommentKind, null);
assert.equal(initialSnapshot.parentCommentPubkey, null);
assert.deepStrictEqual(initialSnapshot.topLevelIds, [
"comment-2",
"comment-1",
@@ -241,6 +248,8 @@ test(
assert.equal(fetchTargets.length, 1, "fetch should run even without an address");
assert.deepStrictEqual(fetchTargets[0], {
videoEventId: "video123",
videoKind: "30078",
videoAuthorPubkey: "authorpk",
parentCommentId: "",
});
assert.equal(
@@ -268,6 +277,9 @@ test(
assert.equal(errors.length, 0, "missing address should not emit errors");
assert.equal(snapshot.videoEventId, "video123");
assert.equal(snapshot.videoKind, "30078");
assert.equal(snapshot.videoAuthorPubkey, "authorpk");
assert.equal(snapshot.videoDefinitionAddress, null);
},
);
+44
View File
@@ -230,6 +230,50 @@ test("publishComment accepts legacy targets with only an event id", async () =>
);
});
test("publishComment derives root and parent metadata from parent comment tags", async () => {
const { client } = createMockClient();
const parentEvent = {
id: "parent-comment",
kind: COMMENT_EVENT_KIND,
pubkey: "parentpk",
tags: [
["E", "video-event", "wss://root", "videopk"],
["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"],
],
};
const result = await publishComment(
client,
{ parentComment: parentEvent },
{ content: "Reply" },
{
resolveActiveSigner: () => null,
shouldRequestExtensionPermissions: () => false,
signEventWithPrivateKey: (event) => ({ ...event, id: "signed" }),
DEFAULT_NIP07_PERMISSION_METHODS: [],
},
);
assert.equal(result.ok, true, "publish should succeed when deriving metadata");
const tags = buildCommentEventTags(result.event);
assert.deepEqual(tags, [
["A", "30078:videopk:root", "wss://definition"],
["K", "30078"],
["P", "videopk", "wss://author"],
["a", "30078:videopk:root", "wss://definition"],
["e", "parent-comment", "wss://parent", "parentpk"],
["k", String(COMMENT_EVENT_KIND)],
["p", "parentpk", "wss://parent"],
]);
});
test("publishComment emits only video event #e tag when address and parent are absent", async () => {
const { client, publishCalls } = createMockClient();
+77
View File
@@ -3,6 +3,7 @@ import assert from "node:assert/strict";
import { setupModal } from "./video-modal-accessibility.test.mjs";
import VideoModalCommentController from "../js/ui/videoModalCommentController.js";
import { COMMENT_EVENT_KIND } from "../js/nostr/commentEvents.js";
const FOCUSABLE_SELECTOR = [
"a[href]",
@@ -247,6 +248,10 @@ test(
assert.deepStrictEqual(payload, {
videoEventId: "legacy-video",
parentCommentId: null,
videoKind: "30078",
videoAuthorPubkey: "AUTHORPK",
rootKind: "30078",
rootAuthorPubkey: "AUTHORPK",
});
assert.equal(
"videoDefinitionAddress" in payload,
@@ -270,6 +275,78 @@ test(
},
);
test(
"VideoModalCommentController includes parent metadata when replying",
async () => {
const publishCalls = [];
const parentEvent = {
id: "parent-1",
kind: COMMENT_EVENT_KIND,
pubkey: "parentpk",
tags: [],
};
const controller = new VideoModalCommentController({
commentThreadService: {
setCallbacks: () => {},
getCommentEvent: (id) => (id === "parent-1" ? parentEvent : null),
processIncomingEvent: () => {},
},
videoModal: {
setCommentComposerState: () => {},
setCommentStatus: () => {},
resetCommentComposer: () => {},
appendComment: () => {},
},
auth: {
isLoggedIn: () => true,
},
services: {
publishComment: async (payload) => {
publishCalls.push(payload);
return { ok: true, event: { id: "reply", tags: [] } };
},
},
utils: {
normalizeHexPubkey: (value) => value?.toLowerCase?.() || value,
},
});
controller.currentVideo = {
id: "video123",
pubkey: "AUTHORPK",
enableComments: true,
kind: 30078,
};
controller.modalCommentState = {
videoEventId: "video123",
videoDefinitionAddress: null,
videoKind: "30078",
videoAuthorPubkey: "AUTHORPK",
parentCommentId: "parent-1",
parentCommentKind: String(COMMENT_EVENT_KIND),
parentCommentPubkey: null,
};
await controller.handleVideoModalCommentSubmit({
text: "Replying",
parentId: "parent-1",
});
assert.equal(publishCalls.length, 1, "reply should trigger publish call");
const payload = publishCalls[0];
assert.equal(payload.videoEventId, "video123");
assert.equal(payload.videoKind, "30078");
assert.equal(payload.videoAuthorPubkey, "AUTHORPK");
assert.equal(payload.parentCommentId, "parent-1");
assert.equal(payload.parentCommentKind, String(COMMENT_EVENT_KIND));
assert.equal(payload.parentCommentPubkey, "parentpk");
assert.equal(payload.parentAuthorPubkey, "parentpk");
assert.equal(payload.rootAuthorPubkey, "AUTHORPK");
},
);
test("VideoModal comment section exposes aria landmarks and participates in focus trap", async (t) => {
const { window, document, modal, playerModal, trigger, cleanup } = await setupModal();
t.after(cleanup);