Merge pull request #1031 from PR0M3TH3AN/codex/update-commentthreadservice-for-videoauthorpubkey

Preserve raw video author pubkeys for comment threads
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-11-03 21:37:35 -05:00
commit 1a945765d8
2 files changed
+152 -15

No files matched your search

+8 -7
View File
@@ -193,7 +193,7 @@ export default class CommentThreadService {
if (this.videoKind) {
target.videoKind = this.videoKind;
}
const targetVideoAuthorPubkey = this.videoAuthorPubkeyRaw || this.videoAuthorPubkey;
const targetVideoAuthorPubkey = this.getVideoAuthorPubkeyForOutput();
if (targetVideoAuthorPubkey) {
target.videoAuthorPubkey = targetVideoAuthorPubkey;
}
@@ -738,14 +738,17 @@ export default class CommentThreadService {
await hydrationPromise;
}
getVideoAuthorPubkeyForOutput() {
return this.videoAuthorPubkeyRaw || this.videoAuthorPubkey || "";
}
emitThreadReady() {
const payload = {
videoEventId: this.videoEventId,
parentCommentId: this.parentCommentId || null,
videoDefinitionAddress: this.videoAddressPointer || null,
videoKind: this.videoKind || null,
videoAuthorPubkey:
this.videoAuthorPubkeyRaw || this.videoAuthorPubkey || null,
videoAuthorPubkey: this.getVideoAuthorPubkeyForOutput() || null,
parentCommentKind: this.parentCommentKind || null,
parentCommentPubkey: this.parentCommentPubkey || null,
topLevelIds: this.getCommentIdsForParent(null),
@@ -763,8 +766,7 @@ export default class CommentThreadService {
parentCommentId: parentId || null,
videoDefinitionAddress: this.videoAddressPointer || null,
videoKind: this.videoKind || null,
videoAuthorPubkey:
this.videoAuthorPubkeyRaw || this.videoAuthorPubkey || null,
videoAuthorPubkey: this.getVideoAuthorPubkeyForOutput() || null,
parentCommentKind: this.parentCommentKind || null,
parentCommentPubkey: this.parentCommentPubkey || null,
commentIds: [eventId],
@@ -789,8 +791,7 @@ export default class CommentThreadService {
parentCommentId: this.parentCommentId || null,
videoDefinitionAddress: this.videoAddressPointer || null,
videoKind: this.videoKind || null,
videoAuthorPubkey:
this.videoAuthorPubkeyRaw || this.videoAuthorPubkey || null,
videoAuthorPubkey: this.getVideoAuthorPubkeyForOutput() || null,
parentCommentKind: this.parentCommentKind || null,
parentCommentPubkey: this.parentCommentPubkey || null,
commentsById: this.cloneCommentsMap(),
+144 -8
View File
@@ -37,6 +37,56 @@ function createComment({
const tick = (ms = 0) => new Promise((resolve) => setTimeout(resolve, ms));
const originalLocalStorage = globalThis.localStorage;
const originalWindowLocalStorage = globalThis.window?.localStorage;
function createIsolatedLocalStorage() {
const store = new Map();
return {
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
setItem(key, value) {
store.set(String(key), String(value));
},
removeItem(key) {
store.delete(String(key));
},
clear() {
store.clear();
},
key(index) {
return Array.from(store.keys())[index] ?? null;
},
get length() {
return store.size;
},
};
}
test.beforeEach(() => {
const stub = createIsolatedLocalStorage();
globalThis.localStorage = stub;
if (globalThis.window) {
globalThis.window.localStorage = stub;
}
});
test.after(() => {
if (originalLocalStorage) {
globalThis.localStorage = originalLocalStorage;
} else {
delete globalThis.localStorage;
}
if (globalThis.window) {
if (originalWindowLocalStorage) {
globalThis.window.localStorage = originalWindowLocalStorage;
} else {
delete globalThis.window.localStorage;
}
}
});
test(
"listVideoComments accepts builder events without parent ids and filters replies",
async () => {
@@ -242,10 +292,9 @@ test(
videoAuthorPubkey: "authorpk",
parentCommentId: "",
});
assert.deepStrictEqual(lastFetchOptions, {
limit: service.defaultLimit,
relays: ["wss://relay.example"],
});
assert.equal(lastFetchOptions.limit, service.defaultLimit);
assert.deepStrictEqual(lastFetchOptions.relays, ["wss://relay.example"]);
assert.equal(lastFetchOptions.since, 0);
assert.equal(subscribeCalls, 1, "subscribeVideoComments should be invoked");
assert.deepStrictEqual(subscriptionTarget, lastFetchTarget);
@@ -383,10 +432,10 @@ test(
"videoDefinitionAddress should be omitted when unavailable",
);
assert.deepStrictEqual(fetchOptions[0], {
limit: service.defaultLimit,
relays: null,
});
const [firstFetchOptions] = fetchOptions;
assert.equal(firstFetchOptions.limit, service.defaultLimit);
assert.equal(firstFetchOptions.relays, null);
assert.equal(firstFetchOptions.since, 0);
assert.equal(
subscribeTargets.length,
@@ -408,6 +457,93 @@ test(
},
);
test(
"CommentThreadService preserves raw video author pubkeys during hydration fetches",
async () => {
const video = {
id: "video-upper",
pubkey: "AUTHORPK",
kind: 30078,
tags: [["d", "video-root-upper"]],
};
const videoDefinitionAddress = "30078:AUTHORPK:video-root-upper";
const pendingComment = {
id: "upper-comment-1",
...buildCommentEvent({
pubkey: "commenter-upper",
created_at: 1700001200,
videoEventId: video.id,
videoDefinitionAddress,
rootIdentifier: "video-root-upper",
rootKind: "30078",
rootAuthorPubkey: video.pubkey,
content: "Uppercase hydration check",
}),
};
const fetchTargets = [];
let resolveFetch = () => {};
const fetchBlocker = new Promise((resolve) => {
resolveFetch = resolve;
});
const fetchVideoComments = async (target) => {
fetchTargets.push(target);
await fetchBlocker;
if (target.videoAuthorPubkey === video.pubkey) {
return [pendingComment];
}
return [];
};
const subscribeTargets = [];
const service = new CommentThreadService({
fetchVideoComments,
subscribeVideoComments: (target, _options = {}) => {
subscribeTargets.push(target);
return () => {};
},
hydrationDebounceMs: 0,
});
const loadPromise = service.loadThread({ video });
await tick();
assert.equal(fetchTargets.length, 1, "hydration fetch should start immediately");
service.processIncomingEvent(pendingComment);
resolveFetch();
const snapshot = await loadPromise;
assert.equal(
fetchTargets[0].videoAuthorPubkey,
video.pubkey,
"fetch target should prefer the raw video author pubkey",
);
assert.equal(
snapshot.videoAuthorPubkey,
video.pubkey,
"snapshot should retain the raw video author pubkey",
);
assert.equal(
snapshot.commentsById.has(pendingComment.id),
true,
"hydration should include comments published before fetch completion",
);
assert.equal(
subscribeTargets.length,
1,
"subscription should begin after hydration completes",
);
assert.equal(
subscribeTargets[0].videoAuthorPubkey,
video.pubkey,
"subscription target should also retain the raw pubkey",
);
},
);
test(
"CommentThreadService teardown cancels hydration timers",
async () => {