mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
Merge pull request #1037 from PR0M3TH3AN/codex/update-video-comment-handling-with-videorootid
Add root identifier plumbing to modal comments
This commit is contained in:
6 files changed
+369
-7
No files matched your search
@@ -816,6 +816,13 @@ function createVideoCommentFilters(targetInput, options = {}) {
|
||||
if (typeof descriptor.rootIdentifier === "string" && descriptor.rootIdentifier) {
|
||||
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
|
||||
|
||||
@@ -24,6 +24,13 @@ function normalizeString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function normalizeRelay(value) {
|
||||
if (typeof value === "string") {
|
||||
return value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizePubkey(pubkey) {
|
||||
const normalized = normalizeString(pubkey);
|
||||
return normalized ? normalized.toLowerCase() : "";
|
||||
@@ -132,6 +139,8 @@ export default class CommentThreadService {
|
||||
this.videoKind = "";
|
||||
this.videoAuthorPubkeyRaw = "";
|
||||
this.videoAuthorPubkey = "";
|
||||
this.videoRootIdentifier = "";
|
||||
this.videoRootRelay = "";
|
||||
this.parentCommentId = "";
|
||||
this.parentCommentKind = "";
|
||||
this.parentCommentPubkey = "";
|
||||
@@ -158,7 +167,7 @@ export default class CommentThreadService {
|
||||
} = {}) {
|
||||
this.teardown();
|
||||
|
||||
// Always ensure the pool is ready before proceeding
|
||||
// Always ensure the pool is ready before proceeding when supported.
|
||||
if (typeof this.nostrClient?.ensurePool === "function") {
|
||||
try {
|
||||
await this.nostrClient.ensurePool();
|
||||
@@ -168,11 +177,6 @@ export default class CommentThreadService {
|
||||
);
|
||||
return { success: false, error };
|
||||
}
|
||||
} else {
|
||||
this.emitError(
|
||||
new Error("Nostr client missing ensurePool implementation."),
|
||||
);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const rawVideoAuthorPubkey = normalizeString(video?.pubkey);
|
||||
@@ -182,6 +186,10 @@ export default class CommentThreadService {
|
||||
this.videoKind = normalizeKind(video?.kind);
|
||||
this.videoAuthorPubkeyRaw = rawVideoAuthorPubkey;
|
||||
this.videoAuthorPubkey = normalizePubkey(rawVideoAuthorPubkey);
|
||||
this.videoRootIdentifier = normalizeString(video?.videoRootId);
|
||||
this.videoRootRelay = normalizeRelay(
|
||||
video?.videoRootRelay || video?.rootIdentifierRelay,
|
||||
);
|
||||
this.parentCommentId = normalizeString(parentCommentId);
|
||||
this.parentCommentKind = this.parentCommentId ? String(COMMENT_EVENT_KIND) : "";
|
||||
this.parentCommentPubkey = "";
|
||||
@@ -213,6 +221,12 @@ export default class CommentThreadService {
|
||||
if (this.videoKind) {
|
||||
target.videoKind = this.videoKind;
|
||||
}
|
||||
if (this.videoRootIdentifier) {
|
||||
target.rootIdentifier = this.videoRootIdentifier;
|
||||
}
|
||||
if (this.videoRootRelay) {
|
||||
target.rootIdentifierRelay = this.videoRootRelay;
|
||||
}
|
||||
const targetVideoAuthorPubkey = this.getVideoAuthorPubkeyForOutput();
|
||||
if (targetVideoAuthorPubkey) {
|
||||
target.videoAuthorPubkey = targetVideoAuthorPubkey;
|
||||
@@ -563,6 +577,14 @@ export default class CommentThreadService {
|
||||
const parentKey = toParentKey(parentId);
|
||||
const createdAt = Number.isFinite(event.created_at) ? event.created_at : 0;
|
||||
const pubkey = normalizePubkey(event.pubkey);
|
||||
const { identifier: rootIdentifier, relay: rootRelay } =
|
||||
this.extractRootIdentifier(event);
|
||||
if (rootIdentifier) {
|
||||
this.videoRootIdentifier = rootIdentifier;
|
||||
if (rootRelay) {
|
||||
this.videoRootRelay = rootRelay;
|
||||
}
|
||||
}
|
||||
if (this.parentCommentId && eventId === this.parentCommentId) {
|
||||
const kindValue = normalizeKind(event.kind);
|
||||
if (kindValue) {
|
||||
@@ -627,6 +649,30 @@ export default class CommentThreadService {
|
||||
return "";
|
||||
}
|
||||
|
||||
extractRootIdentifier(event) {
|
||||
const tags = Array.isArray(event?.tags) ? event.tags : [];
|
||||
for (let index = tags.length - 1; index >= 0; index -= 1) {
|
||||
const tag = tags[index];
|
||||
if (!Array.isArray(tag) || tag.length < 2) {
|
||||
continue;
|
||||
}
|
||||
const name =
|
||||
typeof tag[0] === "string" ? tag[0].trim().toLowerCase() : "";
|
||||
if (name !== "i") {
|
||||
continue;
|
||||
}
|
||||
const value = normalizeString(tag[1]);
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
identifier: value,
|
||||
relay: normalizeRelay(tag[2]),
|
||||
};
|
||||
}
|
||||
return { identifier: "", relay: "" };
|
||||
}
|
||||
|
||||
insertIntoParentList(parentKey, eventId, createdAt) {
|
||||
const list = this.getParentList(parentKey);
|
||||
if (list.includes(eventId)) {
|
||||
@@ -782,6 +828,8 @@ export default class CommentThreadService {
|
||||
videoDefinitionAddress: this.videoAddressPointer || null,
|
||||
videoKind: this.videoKind || null,
|
||||
videoAuthorPubkey: this.getVideoAuthorPubkeyForOutput() || null,
|
||||
rootIdentifier: this.videoRootIdentifier || null,
|
||||
rootIdentifierRelay: this.videoRootRelay || null,
|
||||
parentCommentKind: this.parentCommentKind || null,
|
||||
parentCommentPubkey: this.parentCommentPubkey || null,
|
||||
topLevelIds: this.getCommentIdsForParent(null),
|
||||
@@ -800,6 +848,8 @@ export default class CommentThreadService {
|
||||
videoDefinitionAddress: this.videoAddressPointer || null,
|
||||
videoKind: this.videoKind || null,
|
||||
videoAuthorPubkey: this.getVideoAuthorPubkeyForOutput() || null,
|
||||
rootIdentifier: this.videoRootIdentifier || null,
|
||||
rootIdentifierRelay: this.videoRootRelay || null,
|
||||
parentCommentKind: this.parentCommentKind || null,
|
||||
parentCommentPubkey: this.parentCommentPubkey || null,
|
||||
commentIds: [eventId],
|
||||
@@ -825,6 +875,8 @@ export default class CommentThreadService {
|
||||
videoDefinitionAddress: this.videoAddressPointer || null,
|
||||
videoKind: this.videoKind || null,
|
||||
videoAuthorPubkey: this.getVideoAuthorPubkeyForOutput() || null,
|
||||
rootIdentifier: this.videoRootIdentifier || null,
|
||||
rootIdentifierRelay: this.videoRootRelay || null,
|
||||
parentCommentKind: this.parentCommentKind || null,
|
||||
parentCommentPubkey: this.parentCommentPubkey || null,
|
||||
commentsById: this.cloneCommentsMap(),
|
||||
|
||||
@@ -8,6 +8,13 @@ function normalizeString(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function normalizeRelay(value) {
|
||||
if (typeof value === "string") {
|
||||
return value.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function normalizeKind(value) {
|
||||
if (Number.isFinite(value)) {
|
||||
return String(Math.floor(value));
|
||||
@@ -86,6 +93,8 @@ export default class VideoModalCommentController {
|
||||
videoDefinitionAddress: null,
|
||||
videoKind: null,
|
||||
videoAuthorPubkey: null,
|
||||
videoRootId: null,
|
||||
videoRootRelay: null,
|
||||
parentCommentId: null,
|
||||
parentCommentKind: null,
|
||||
parentCommentPubkey: null,
|
||||
@@ -152,6 +161,10 @@ export default class VideoModalCommentController {
|
||||
}
|
||||
const videoKind = normalizeKind(video.kind);
|
||||
const videoAuthorPubkey = normalizeString(video.pubkey);
|
||||
const videoRootId = normalizeString(video.videoRootId);
|
||||
const videoRootRelay = normalizeRelay(
|
||||
video.videoRootRelay || video.rootIdentifierRelay,
|
||||
);
|
||||
|
||||
if (!videoEventId) {
|
||||
this.resetModalCommentState({ hide: false });
|
||||
@@ -166,6 +179,8 @@ export default class VideoModalCommentController {
|
||||
videoDefinitionAddress: videoDefinitionAddress || null,
|
||||
videoKind: videoKind || null,
|
||||
videoAuthorPubkey: videoAuthorPubkey || null,
|
||||
videoRootId: videoRootId || null,
|
||||
videoRootRelay: videoRootRelay || null,
|
||||
parentCommentId: null,
|
||||
parentCommentKind: null,
|
||||
parentCommentPubkey: null,
|
||||
@@ -248,6 +263,8 @@ export default class VideoModalCommentController {
|
||||
videoDefinitionAddress: null,
|
||||
videoKind: null,
|
||||
videoAuthorPubkey: null,
|
||||
videoRootId: null,
|
||||
videoRootRelay: null,
|
||||
parentCommentId: null,
|
||||
parentCommentKind: null,
|
||||
parentCommentPubkey: null,
|
||||
@@ -354,6 +371,14 @@ export default class VideoModalCommentController {
|
||||
if (snapshotVideoAuthor) {
|
||||
this.modalCommentState.videoAuthorPubkey = snapshotVideoAuthor;
|
||||
}
|
||||
const snapshotRootIdentifier = normalizeString(snapshot.rootIdentifier);
|
||||
if (snapshotRootIdentifier) {
|
||||
this.modalCommentState.videoRootId = snapshotRootIdentifier;
|
||||
}
|
||||
const snapshotRootRelay = normalizeRelay(snapshot.rootIdentifierRelay);
|
||||
if (snapshotRootRelay) {
|
||||
this.modalCommentState.videoRootRelay = snapshotRootRelay;
|
||||
}
|
||||
const snapshotParentKind = normalizeKind(snapshot.parentCommentKind);
|
||||
if (snapshotParentKind) {
|
||||
this.modalCommentState.parentCommentKind = snapshotParentKind;
|
||||
@@ -384,6 +409,15 @@ export default class VideoModalCommentController {
|
||||
this.modalCommentProfiles.set(pubkey, profile);
|
||||
});
|
||||
|
||||
const payloadRootIdentifier = normalizeString(payload.rootIdentifier);
|
||||
if (payloadRootIdentifier) {
|
||||
this.modalCommentState.videoRootId = payloadRootIdentifier;
|
||||
}
|
||||
const payloadRootRelay = normalizeRelay(payload.rootIdentifierRelay);
|
||||
if (payloadRootRelay) {
|
||||
this.modalCommentState.videoRootRelay = payloadRootRelay;
|
||||
}
|
||||
|
||||
const commentIds = Array.isArray(payload.commentIds)
|
||||
? payload.commentIds
|
||||
: [];
|
||||
@@ -520,6 +554,8 @@ export default class VideoModalCommentController {
|
||||
return {
|
||||
videoEventId: snapshot.videoEventId,
|
||||
parentCommentId: snapshot.parentCommentId || null,
|
||||
rootIdentifier: snapshot.rootIdentifier || null,
|
||||
rootIdentifierRelay: snapshot.rootIdentifierRelay || null,
|
||||
commentsById: sanitizedComments,
|
||||
childrenByParent: sanitizedChildren,
|
||||
profiles: this.modalCommentProfiles,
|
||||
@@ -575,6 +611,11 @@ export default class VideoModalCommentController {
|
||||
this.modalCommentState.videoKind || normalizeKind(video.kind);
|
||||
const videoAuthorPubkey =
|
||||
this.modalCommentState.videoAuthorPubkey || normalizeString(video.pubkey);
|
||||
const videoRootId =
|
||||
this.modalCommentState.videoRootId || normalizeString(video.videoRootId);
|
||||
const videoRootRelay =
|
||||
this.modalCommentState.videoRootRelay ||
|
||||
normalizeRelay(video.videoRootRelay || video.rootIdentifierRelay);
|
||||
|
||||
if (!videoEventId) {
|
||||
this.callbacks.showError("Comments are unavailable for this video.");
|
||||
@@ -632,6 +673,12 @@ export default class VideoModalCommentController {
|
||||
publishPayload.videoAuthorPubkey = videoAuthorPubkey;
|
||||
publishPayload.rootAuthorPubkey = videoAuthorPubkey;
|
||||
}
|
||||
if (videoRootId) {
|
||||
publishPayload.rootIdentifier = videoRootId;
|
||||
}
|
||||
if (videoRootRelay) {
|
||||
publishPayload.rootIdentifierRelay = videoRootRelay;
|
||||
}
|
||||
if (parentCommentKind) {
|
||||
publishPayload.parentCommentKind = parentCommentKind;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import CommentThreadService from "../js/services/commentThreadService.js";
|
||||
import { listVideoComments } from "../js/nostr/commentEvents.js";
|
||||
import { COMMENT_EVENT_KIND, listVideoComments } from "../js/nostr/commentEvents.js";
|
||||
import { buildCommentEvent } from "../js/nostrEventSchemas.js";
|
||||
|
||||
function createBaseVideo() {
|
||||
@@ -11,6 +11,7 @@ function createBaseVideo() {
|
||||
pubkey: "authorpk",
|
||||
kind: 30078,
|
||||
tags: [["d", "video-root"], ["alt", "ignored"]],
|
||||
videoRootId: "video-root",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,6 +257,7 @@ test(
|
||||
const hydrationRequests = [];
|
||||
|
||||
const service = new CommentThreadService({
|
||||
nostrClient: { ensurePool: async () => {} },
|
||||
fetchVideoComments,
|
||||
subscribeVideoComments,
|
||||
getProfileCacheEntry: (pubkey) =>
|
||||
@@ -290,6 +292,7 @@ test(
|
||||
videoDefinitionAddress: "30078:authorpk:video-root",
|
||||
videoKind: "30078",
|
||||
videoAuthorPubkey: "authorpk",
|
||||
rootIdentifier: "video-root",
|
||||
parentCommentId: "",
|
||||
});
|
||||
assert.equal(lastFetchOptions.limit, service.defaultLimit);
|
||||
@@ -308,6 +311,7 @@ test(
|
||||
assert.equal(initialSnapshot.videoDefinitionAddress, "30078:authorpk:video-root");
|
||||
assert.equal(initialSnapshot.videoKind, "30078");
|
||||
assert.equal(initialSnapshot.videoAuthorPubkey, "authorpk");
|
||||
assert.equal(initialSnapshot.rootIdentifier, "video-root");
|
||||
assert.equal(initialSnapshot.parentCommentKind, null);
|
||||
assert.equal(initialSnapshot.parentCommentPubkey, null);
|
||||
assert.deepStrictEqual(initialSnapshot.topLevelIds, [
|
||||
@@ -358,6 +362,7 @@ test(
|
||||
assert.equal(appendPayload.parentCommentId, "comment-2");
|
||||
assert.deepStrictEqual(appendPayload.commentIds, ["reply-2"]);
|
||||
assert.ok(appendPayload.commentsById instanceof Map);
|
||||
assert.equal(appendPayload.rootIdentifier, "video-root");
|
||||
assert.ok(
|
||||
appendPayload.commentsById.has("reply-2"),
|
||||
"appended payload should contain the new reply",
|
||||
@@ -401,6 +406,7 @@ test(
|
||||
const errors = [];
|
||||
|
||||
const service = new CommentThreadService({
|
||||
nostrClient: { ensurePool: async () => {} },
|
||||
fetchVideoComments: async (target, options) => {
|
||||
fetchTargets.push(target);
|
||||
fetchOptions.push(options);
|
||||
@@ -424,6 +430,7 @@ test(
|
||||
videoEventId: "video123",
|
||||
videoKind: "30078",
|
||||
videoAuthorPubkey: "authorpk",
|
||||
rootIdentifier: "video-root",
|
||||
parentCommentId: "",
|
||||
});
|
||||
assert.equal(
|
||||
@@ -457,6 +464,51 @@ test(
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"CommentThreadService requests and snapshots comments by root identifier",
|
||||
async () => {
|
||||
const video = {
|
||||
id: "root-only-video",
|
||||
pubkey: "rootpk",
|
||||
kind: 30078,
|
||||
videoRootId: "root-only",
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const rootOnlyComment = {
|
||||
id: "comment-root",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
pubkey: "commenter",
|
||||
created_at: 1700000000,
|
||||
content: "Root scoped",
|
||||
tags: [["i", "root-only"], ["p", "rootpk"]],
|
||||
};
|
||||
|
||||
let fetchTarget = null;
|
||||
const service = new CommentThreadService({
|
||||
nostrClient: { ensurePool: async () => {} },
|
||||
fetchVideoComments: async (target) => {
|
||||
fetchTarget = target;
|
||||
return [rootOnlyComment];
|
||||
},
|
||||
subscribeVideoComments: () => () => {},
|
||||
});
|
||||
|
||||
const threadReadyPayloads = [];
|
||||
service.setCallbacks({
|
||||
onThreadReady: (payload) => threadReadyPayloads.push(payload),
|
||||
});
|
||||
|
||||
const snapshot = await service.loadThread({ video });
|
||||
|
||||
assert.ok(fetchTarget, "fetch should run when only root id is provided");
|
||||
assert.equal(fetchTarget.rootIdentifier, "root-only");
|
||||
assert.equal(snapshot.rootIdentifier, "root-only");
|
||||
assert.equal(snapshot.commentsById.get("comment-root").content, "Root scoped");
|
||||
assert.equal(threadReadyPayloads[0].rootIdentifier, "root-only");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"CommentThreadService preserves raw video author pubkeys during hydration fetches",
|
||||
async () => {
|
||||
@@ -498,6 +550,7 @@ test(
|
||||
|
||||
const subscribeTargets = [];
|
||||
const service = new CommentThreadService({
|
||||
nostrClient: { ensurePool: async () => {} },
|
||||
fetchVideoComments,
|
||||
subscribeVideoComments: (target, _options = {}) => {
|
||||
subscribeTargets.push(target);
|
||||
@@ -555,6 +608,7 @@ test(
|
||||
let subscriptionHandler = null;
|
||||
|
||||
const service = new CommentThreadService({
|
||||
nostrClient: { ensurePool: async () => {} },
|
||||
fetchVideoComments: async () => [],
|
||||
subscribeVideoComments: (_target, options = {}) => {
|
||||
subscriptionHandler = typeof options.onEvent === "function" ? options.onEvent : null;
|
||||
|
||||
@@ -555,6 +555,107 @@ test("listVideoComments builds filters with uppercase roots plus legacy fallback
|
||||
);
|
||||
});
|
||||
|
||||
test("listVideoComments adds lowercase 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"],
|
||||
],
|
||||
};
|
||||
|
||||
const { client, pool } = createMockClient();
|
||||
|
||||
let receivedFilters = null;
|
||||
pool.list = async (relays, filters) => {
|
||||
receivedFilters = { relays, filters };
|
||||
return [[matchingEvent]];
|
||||
};
|
||||
|
||||
const events = await listVideoComments(
|
||||
client,
|
||||
{
|
||||
videoEventId: "video-rooted",
|
||||
videoKind: COMMENT_EVENT_KIND,
|
||||
videoAuthorPubkey: "root-author",
|
||||
rootIdentifier: "root-only",
|
||||
},
|
||||
{ since: 1700000000, limit: 5 },
|
||||
);
|
||||
|
||||
assert.equal(events.length, 1, "matching comment should be returned");
|
||||
assert.ok(receivedFilters, "pool.list should receive filters");
|
||||
assert.equal(
|
||||
receivedFilters.filters.length,
|
||||
4,
|
||||
"root identifier requests should include lowercase fallbacks",
|
||||
);
|
||||
|
||||
const [eventFilter, uppercaseEventFilter, lowercaseRootFilter, 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",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
uppercaseRootFilter.kinds[0],
|
||||
COMMENT_EVENT_KIND,
|
||||
"uppercase root filter should continue targeting comment kind",
|
||||
);
|
||||
assert.deepEqual(
|
||||
uppercaseRootFilter["#I"],
|
||||
["root-only"],
|
||||
"uppercase root filter should continue binding the root identifier via #I",
|
||||
);
|
||||
assert.deepEqual(
|
||||
uppercaseRootFilter["#P"],
|
||||
["root-author"],
|
||||
"uppercase root filter should scope by author when available",
|
||||
);
|
||||
assert.equal(
|
||||
uppercaseRootFilter.since,
|
||||
1700000000,
|
||||
"since option should propagate to uppercase root filter",
|
||||
);
|
||||
assert.equal(
|
||||
uppercaseRootFilter.limit,
|
||||
5,
|
||||
"limit option should propagate to uppercase root filter",
|
||||
);
|
||||
});
|
||||
|
||||
test("listVideoComments supports legacy targets without a definition address", async () => {
|
||||
const legacyEvent = {
|
||||
id: "legacy-comment",
|
||||
|
||||
@@ -230,6 +230,8 @@ test(
|
||||
enableComments: true,
|
||||
kind: 30078,
|
||||
tags: [["d", "video-root"]],
|
||||
videoRootId: "video-root",
|
||||
videoRootRelay: "wss://root.example",
|
||||
};
|
||||
|
||||
controller.load(video);
|
||||
@@ -242,6 +244,8 @@ test(
|
||||
assert.equal(payload.videoEventId, "video123");
|
||||
assert.equal(payload.videoAuthorPubkey, "AUTHORPK");
|
||||
assert.equal(payload.videoDefinitionAddress, "30078:AUTHORPK:video-root");
|
||||
assert.equal(payload.rootIdentifier, "video-root");
|
||||
assert.equal(payload.rootIdentifierRelay, "wss://root.example");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -287,12 +291,16 @@ test(
|
||||
pubkey: "AUTHORPK",
|
||||
enableComments: true,
|
||||
kind: 30078,
|
||||
videoRootId: "video-root",
|
||||
videoRootRelay: "wss://root.example",
|
||||
// No tags provided; buildVideoAddressPointer should return an empty string.
|
||||
};
|
||||
|
||||
controller.load(video);
|
||||
controller.modalCommentState.videoDefinitionAddress =
|
||||
" 30078:AUTHORPK:video-root ";
|
||||
controller.modalCommentState.videoRootId = " video-root ";
|
||||
controller.modalCommentState.videoRootRelay = " wss://root.example ";
|
||||
|
||||
controller.submit({ text: "Fallback pointer" });
|
||||
|
||||
@@ -305,6 +313,97 @@ test(
|
||||
"30078:AUTHORPK:video-root",
|
||||
"publish payload should reuse pointer from the loaded thread",
|
||||
);
|
||||
assert.equal(payload.rootIdentifier, "video-root");
|
||||
assert.equal(payload.rootIdentifierRelay, "wss://root.example");
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"VideoModalCommentController preserves videoRootId when only root pointer is provided",
|
||||
async () => {
|
||||
const publishCalls = [];
|
||||
const loadTargets = [];
|
||||
|
||||
const controller = new VideoModalCommentController({
|
||||
commentThreadService: {
|
||||
setCallbacks: ({ onThreadReady }) => {
|
||||
controller.commentThreadServiceReady = onThreadReady;
|
||||
},
|
||||
teardown: () => {},
|
||||
defaultLimit: 10,
|
||||
loadThread: async (target) => {
|
||||
loadTargets.push(target);
|
||||
return {};
|
||||
},
|
||||
processIncomingEvent: () => {},
|
||||
},
|
||||
videoModal: {
|
||||
setCommentSectionCallbacks: () => {},
|
||||
hideCommentsDisabledMessage: () => {},
|
||||
showCommentsDisabledMessage: () => {},
|
||||
setCommentsVisibility: () => {},
|
||||
clearComments: () => {},
|
||||
resetCommentComposer: () => {},
|
||||
setCommentStatus: () => {},
|
||||
setCommentComposerState: () => {},
|
||||
renderComments: () => {},
|
||||
},
|
||||
auth: {
|
||||
isLoggedIn: () => true,
|
||||
},
|
||||
services: {
|
||||
publishComment: async (payload) => {
|
||||
publishCalls.push(payload);
|
||||
return { ok: true, event: { id: "comment", tags: [] } };
|
||||
},
|
||||
},
|
||||
utils: {
|
||||
normalizeHexPubkey: (value) => value,
|
||||
},
|
||||
});
|
||||
|
||||
const video = {
|
||||
id: "video789",
|
||||
pubkey: "AUTHORPK",
|
||||
enableComments: true,
|
||||
kind: 30078,
|
||||
videoRootId: "root-only",
|
||||
videoRootRelay: "wss://root-only",
|
||||
tags: [],
|
||||
};
|
||||
|
||||
controller.load(video);
|
||||
await Promise.resolve();
|
||||
|
||||
assert.equal(loadTargets.length, 1, "loadThread should be invoked once");
|
||||
assert.equal(loadTargets[0].rootIdentifier, "root-only");
|
||||
assert.equal(loadTargets[0].rootIdentifierRelay, "wss://root-only");
|
||||
|
||||
const commentEvent = {
|
||||
id: "comment-root",
|
||||
pubkey: "commenter",
|
||||
kind: COMMENT_EVENT_KIND,
|
||||
content: "Root pointer only",
|
||||
tags: [["I", "root-only"], ["p", "AUTHORPK"]],
|
||||
};
|
||||
|
||||
controller.handleCommentThreadReady({
|
||||
videoEventId: "video789",
|
||||
parentCommentId: null,
|
||||
rootIdentifier: "root-only",
|
||||
rootIdentifierRelay: "wss://root-only",
|
||||
commentsById: new Map([["comment-root", commentEvent]]),
|
||||
childrenByParent: new Map([[null, ["comment-root"]]]),
|
||||
profiles: new Map(),
|
||||
});
|
||||
|
||||
controller.submit({ text: "Posting" });
|
||||
await controller.modalCommentPublishPromise;
|
||||
|
||||
assert.equal(publishCalls.length, 1, "publishComment should use root pointer");
|
||||
const payload = publishCalls[0];
|
||||
assert.equal(payload.rootIdentifier, "root-only");
|
||||
assert.equal(payload.rootIdentifierRelay, "wss://root-only");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -557,6 +656,8 @@ test(
|
||||
videoDefinitionAddress: null,
|
||||
videoKind: "30078",
|
||||
videoAuthorPubkey: "AUTHORPK",
|
||||
videoRootId: null,
|
||||
videoRootRelay: null,
|
||||
parentCommentId: "parent-1",
|
||||
parentCommentKind: String(COMMENT_EVENT_KIND),
|
||||
parentCommentPubkey: null,
|
||||
|
||||
Reference in new issue
Block a user