mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #1007 from PR0M3TH3AN/codex/update-editvideo-flow-to-reuse-d-tag
Preserve d tag when editing nostr videos
This commit is contained in:
2 files changed
+146
-11
No files matched your search
+48
-11
@@ -2217,6 +2217,44 @@ export class NostrClient {
|
||||
return resolveActiveSigner(normalizedPubkey || extensionPubkey);
|
||||
}
|
||||
|
||||
resolveEventDTag(event, fallbackEvent = null) {
|
||||
const immediate = getDTagValueFromTags(event?.tags);
|
||||
if (immediate) {
|
||||
return immediate;
|
||||
}
|
||||
|
||||
const fallbackTag = getDTagValueFromTags(fallbackEvent?.tags);
|
||||
if (fallbackTag) {
|
||||
return fallbackTag;
|
||||
}
|
||||
|
||||
const candidateIds = [];
|
||||
const pushCandidate = (candidate) => {
|
||||
if (typeof candidate === "string" && candidate.trim()) {
|
||||
const normalized = candidate.trim();
|
||||
if (!candidateIds.includes(normalized)) {
|
||||
candidateIds.push(normalized);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pushCandidate(event?.id);
|
||||
pushCandidate(fallbackEvent?.id);
|
||||
|
||||
for (const candidateId of candidateIds) {
|
||||
const rawEvent = this.rawEvents?.get?.(candidateId);
|
||||
if (!rawEvent) {
|
||||
continue;
|
||||
}
|
||||
const rawTag = getDTagValueFromTags(rawEvent.tags);
|
||||
if (rawTag) {
|
||||
return rawTag;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
applyRootCreatedAt(video) {
|
||||
if (!video || typeof video !== "object") {
|
||||
return null;
|
||||
@@ -4283,8 +4321,9 @@ export class NostrClient {
|
||||
// Use the existing videoRootId (or fall back to the base event's ID)
|
||||
const oldRootId = baseEvent.videoRootId || baseEvent.id;
|
||||
|
||||
// Generate a new d-tag so that the edit gets its own share link
|
||||
const newD = `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
||||
const preservedDTag = this.resolveEventDTag(baseEvent, originalEventStub);
|
||||
const finalDTagValue =
|
||||
preservedDTag || `${Date.now()}-edit-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
// Build the updated content object
|
||||
const contentObject = {
|
||||
@@ -4329,13 +4368,13 @@ export class NostrClient {
|
||||
const event = buildVideoPostEvent({
|
||||
pubkey: userPubkeyLower,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
dTagValue: newD,
|
||||
dTagValue: finalDTagValue,
|
||||
content: contentObject,
|
||||
additionalTags: nip71Tags,
|
||||
});
|
||||
|
||||
devLogger.log("Creating edited event with root ID:", oldRootId);
|
||||
devLogger.log("Event content:", event.content);
|
||||
devLogger.log("Event content:", event.content);
|
||||
|
||||
await this.ensureActiveSignerForPubkey(userPubkeyLower);
|
||||
|
||||
@@ -4458,9 +4497,7 @@ export class NostrClient {
|
||||
};
|
||||
}
|
||||
|
||||
const safeTags = Array.isArray(baseEvent.tags) ? baseEvent.tags : [];
|
||||
const dTag = safeTags.find((t) => t[0] === "d");
|
||||
const existingD = dTag ? dTag[1] : null;
|
||||
const existingD = this.resolveEventDTag(baseEvent, originalEvent) || null;
|
||||
|
||||
let oldContent = {};
|
||||
try {
|
||||
@@ -4626,7 +4663,7 @@ export class NostrClient {
|
||||
(targetVideo && typeof targetVideo.videoRootId === "string"
|
||||
? targetVideo.videoRootId.trim()
|
||||
: "");
|
||||
const targetDTag = targetVideo ? getDTagValueFromTags(targetVideo.tags) : "";
|
||||
const targetDTag = this.resolveEventDTag(targetVideo);
|
||||
|
||||
if (targetVideo) {
|
||||
try {
|
||||
@@ -4656,7 +4693,7 @@ export class NostrClient {
|
||||
|
||||
const candidateRoot =
|
||||
typeof candidate.videoRootId === "string" ? candidate.videoRootId : "";
|
||||
const candidateDTag = getDTagValueFromTags(candidate.tags);
|
||||
const candidateDTag = this.resolveEventDTag(candidate);
|
||||
const sameRoot = inferredRoot && candidateRoot === inferredRoot;
|
||||
const sameD = targetDTag && candidateDTag === targetDTag;
|
||||
const legacyMatch =
|
||||
@@ -5843,7 +5880,7 @@ export class NostrClient {
|
||||
const targetRoot = typeof video.videoRootId === "string" ? video.videoRootId : "";
|
||||
const targetPubkey = typeof video.pubkey === "string" ? video.pubkey.toLowerCase() : "";
|
||||
|
||||
const targetDTag = getDTagValueFromTags(video.tags);
|
||||
const targetDTag = this.resolveEventDTag(video);
|
||||
|
||||
const collectLocalMatches = () => {
|
||||
const seen = new Set();
|
||||
@@ -5863,7 +5900,7 @@ export class NostrClient {
|
||||
|
||||
const candidateRoot =
|
||||
typeof candidate.videoRootId === "string" ? candidate.videoRootId : "";
|
||||
const candidateDTag = getDTagValueFromTags(candidate.tags);
|
||||
const candidateDTag = this.resolveEventDTag(candidate, video);
|
||||
|
||||
const sameRoot = targetRoot && candidateRoot === targetRoot;
|
||||
const sameD = targetDTag && candidateDTag === targetDTag;
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
NostrClient,
|
||||
setActiveSigner,
|
||||
clearActiveSigner,
|
||||
} from "../../js/nostr.js";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
function createStubPool() {
|
||||
return {
|
||||
publish(urls, event) {
|
||||
noop(event);
|
||||
return {
|
||||
on(type, handler) {
|
||||
if (type === "ok") {
|
||||
handler();
|
||||
return true;
|
||||
}
|
||||
if (type === "failed") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("editVideo preserves existing d tag", async (t) => {
|
||||
const client = new NostrClient();
|
||||
const pubkey = "ABC123";
|
||||
const existingD = "stable-d-tag";
|
||||
const baseEvent = {
|
||||
id: "event-original",
|
||||
pubkey,
|
||||
version: 3,
|
||||
videoRootId: "root-abc",
|
||||
title: "Original Title",
|
||||
url: "https://example.com/video.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:example",
|
||||
ws: "wss://example.com",
|
||||
xs: "https://example.com/file.torrent",
|
||||
enableComments: true,
|
||||
isPrivate: false,
|
||||
isNsfw: false,
|
||||
isForKids: false,
|
||||
nip71: null,
|
||||
thumbnail: "https://example.com/thumb.jpg",
|
||||
description: "original",
|
||||
mode: "live",
|
||||
tags: [
|
||||
["t", "video"],
|
||||
["d", existingD],
|
||||
],
|
||||
};
|
||||
|
||||
client.getEventById = async (id) => (id === baseEvent.id ? baseEvent : null);
|
||||
client.relays = ["wss://relay.one"];
|
||||
client.pool = createStubPool();
|
||||
client.ensureExtensionPermissions = async () => ({ ok: true });
|
||||
|
||||
const signedEvents = [];
|
||||
const signer = {
|
||||
type: "app",
|
||||
pubkey,
|
||||
signEvent: async (event) => {
|
||||
signedEvents.push(event);
|
||||
return { ...event, id: `signed-${signedEvents.length}` };
|
||||
},
|
||||
};
|
||||
|
||||
setActiveSigner(signer);
|
||||
client.ensureActiveSignerForPubkey = async () => signer;
|
||||
|
||||
t.after(() => {
|
||||
clearActiveSigner();
|
||||
signedEvents.length = 0;
|
||||
});
|
||||
|
||||
const result = await client.editVideo({ id: baseEvent.id }, { title: "Updated" }, pubkey);
|
||||
|
||||
assert.ok(result, "editVideo should return the signed event");
|
||||
assert.equal(signedEvents.length, 1, "editVideo should sign exactly one event");
|
||||
const signedEvent = signedEvents[0];
|
||||
assert.ok(signedEvent, "signed event should be captured");
|
||||
const dTags = signedEvent.tags.filter((tag) => Array.isArray(tag) && tag[0] === "d");
|
||||
assert.deepEqual(dTags, [["d", existingD]], "editVideo should preserve the existing d tag");
|
||||
|
||||
const parsedContent = JSON.parse(signedEvent.content);
|
||||
assert.equal(
|
||||
parsedContent.videoRootId,
|
||||
baseEvent.videoRootId,
|
||||
"editVideo should preserve the original videoRootId",
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user