Allow comments without definition address

This commit is contained in:
thePR0M3TH3AN committed 2025-10-29 09:20:05 -04:00
1 parent 38d706737f
commit a4e6db7151
4 files changed
+180 -10

No files matched your search

+5 -2
View File
@@ -141,7 +141,7 @@ export default class CommentThreadService {
this.parentCommentId = normalizeString(parentCommentId);
this.activeRelays = Array.isArray(relays) ? [...relays] : null;
if (!this.videoEventId || !this.videoAddressPointer) {
if (!this.videoEventId) {
this.emitError(
new Error("Unable to resolve video pointer for comment thread."),
);
@@ -158,10 +158,13 @@ export default class CommentThreadService {
const fetchLimit = toPositiveInteger(limit, this.defaultLimit);
const target = {
videoEventId: this.videoEventId,
videoDefinitionAddress: this.videoAddressPointer,
parentCommentId: this.parentCommentId,
};
if (this.videoAddressPointer) {
target.videoDefinitionAddress = this.videoAddressPointer;
}
let events = [];
try {
events = await this.fetchVideoComments(target, {
+13 -8
View File
@@ -132,7 +132,7 @@ export default class VideoModalCommentController {
const videoEventId = normalizeString(video.id);
const videoDefinitionAddress = buildVideoAddressPointer(video);
if (!videoEventId || !videoDefinitionAddress) {
if (!videoEventId) {
this.resetModalCommentState({ hide: false });
this.videoModal.setCommentStatus?.(
"Comments are unavailable for this video.",
@@ -142,7 +142,7 @@ export default class VideoModalCommentController {
this.modalCommentState = {
videoEventId,
videoDefinitionAddress,
videoDefinitionAddress: videoDefinitionAddress || null,
parentCommentId: null,
};
this.modalCommentProfiles = new Map();
@@ -487,7 +487,7 @@ export default class VideoModalCommentController {
const videoEventId = normalizeString(video.id);
const videoDefinitionAddress = buildVideoAddressPointer(video);
if (!videoEventId || !videoDefinitionAddress) {
if (!videoEventId) {
this.callbacks.showError("Comments are unavailable for this video.");
return;
}
@@ -499,13 +499,18 @@ export default class VideoModalCommentController {
reason: "submitting",
});
const publishPayload = {
videoEventId,
parentCommentId,
};
if (videoDefinitionAddress) {
publishPayload.videoDefinitionAddress = videoDefinitionAddress;
}
const publishPromise = Promise.resolve(
this.services.publishComment(
{
videoEventId,
videoDefinitionAddress,
parentCommentId,
},
publishPayload,
{
content: text,
},
+66
View File
@@ -205,6 +205,72 @@ test(
},
);
test(
"CommentThreadService loadThread falls back to event id when address is missing",
async () => {
const video = {
...createBaseVideo(),
tags: [["alt", "ignored"]],
};
const fetchTargets = [];
const fetchOptions = [];
const subscribeTargets = [];
const subscribeOptions = [];
const errors = [];
const service = new CommentThreadService({
fetchVideoComments: async (target, options) => {
fetchTargets.push(target);
fetchOptions.push(options);
return [];
},
subscribeVideoComments: (target, options = {}) => {
subscribeTargets.push(target);
subscribeOptions.push(options);
return () => {};
},
});
service.setCallbacks({
onError: (error) => errors.push(error),
});
const snapshot = await service.loadThread({ video });
assert.equal(fetchTargets.length, 1, "fetch should run even without an address");
assert.deepStrictEqual(fetchTargets[0], {
videoEventId: "video123",
parentCommentId: "",
});
assert.equal(
"videoDefinitionAddress" in fetchTargets[0],
false,
"videoDefinitionAddress should be omitted when unavailable",
);
assert.deepStrictEqual(fetchOptions[0], {
limit: service.defaultLimit,
relays: null,
});
assert.equal(
subscribeTargets.length,
1,
"subscribe should run even without an address",
);
assert.deepStrictEqual(subscribeTargets[0], fetchTargets[0]);
assert.equal(
typeof subscribeOptions[0]?.onEvent,
"function",
"subscription handler should be provided",
);
assert.equal(errors.length, 0, "missing address should not emit errors");
assert.equal(snapshot.videoEventId, "video123");
},
);
test(
"CommentThreadService teardown cancels hydration timers",
async () => {
+96
View File
@@ -2,6 +2,7 @@ import test from "node:test";
import assert from "node:assert/strict";
import { setupModal } from "./video-modal-accessibility.test.mjs";
import VideoModalCommentController from "../js/ui/videoModalCommentController.js";
const FOCUSABLE_SELECTOR = [
"a[href]",
@@ -174,6 +175,101 @@ test("VideoModal comment composer updates messaging and dispatches events", asyn
assert.strictEqual(submitDetail.triggerElement, modal.commentsSubmitButton);
});
test(
"VideoModalCommentController publishes comment using event id fallback",
async () => {
const publishCalls = [];
const statusMessages = [];
let resetCalled = false;
const composerStates = [];
const appendedEvents = [];
const videoModal = {
setCommentComposerState: (state) => {
composerStates.push({ ...state });
},
resetCommentComposer: () => {
resetCalled = true;
},
setCommentStatus: (message) => {
statusMessages.push(message);
},
appendComment: (event) => {
appendedEvents.push(event);
},
};
const processedEvents = [];
const controller = new VideoModalCommentController({
commentThreadService: {
setCallbacks: () => {},
processIncomingEvent: (event) => {
processedEvents.push(event);
},
},
videoModal,
auth: {
isLoggedIn: () => true,
},
services: {
publishComment: async (payload, eventData) => {
publishCalls.push([payload, eventData]);
return {
ok: true,
event: {
id: "legacy-comment",
pubkey: "pk1",
content: eventData.content,
tags: [],
},
};
},
},
utils: {
normalizeHexPubkey: (value) => value?.toLowerCase?.() || value,
},
});
controller.currentVideo = {
id: "legacy-video",
pubkey: "AUTHORPK",
enableComments: true,
kind: 30078,
tags: [],
};
controller.modalCommentState.videoEventId = "legacy-video";
await controller.handleVideoModalCommentSubmit({ text: "Legacy support" });
assert.equal(publishCalls.length, 1, "publishComment should be invoked");
const [payload, eventData] = publishCalls[0];
assert.deepStrictEqual(payload, {
videoEventId: "legacy-video",
parentCommentId: null,
});
assert.equal(
"videoDefinitionAddress" in payload,
false,
"payload should omit videoDefinitionAddress when unavailable",
);
assert.equal(eventData.content, "Legacy support");
assert.equal(processedEvents.length, 1, "event should be processed optimistically");
assert.equal(resetCalled, true, "composer should reset after publishing");
assert.ok(
statusMessages.includes("Comment posted."),
"success message should be surfaced",
);
assert.equal(
composerStates[composerStates.length - 1]?.disabled,
false,
"composer should be re-enabled after publishing",
);
assert.equal(appendedEvents.length, 0, "optimistic insert should avoid duplicate append");
},
);
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);