mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-29 08:52:25 +00:00
Merge pull request #995 from PR0M3TH3AN/codex/enhance-comment-metadata-handling
Improve comment metadata propagation
This commit is contained in:
6 files changed
+655
-42
No files matched your search
+350
-42
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user