Merge pull request #2734 from PR0M3TH3AN/refactor/revert-modal-rendering-15253432646628070109

Refactor RevertModal rendering logic
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-26 13:00:38 -05:00
commit c28d1e2c2b
3 files changed
+696 -449

No files matched your search

+32 -449
View File
@@ -1,5 +1,6 @@
import { Nip71FormManager } from "./nip71FormManager.js";
import { createModalAccessibility } from "./modalAccessibility.js";
import { getDTagValueFromTags } from "../../nostr/nip71.js";
import {
createImetaVariants,
createTextTracks,
@@ -7,6 +8,12 @@ import {
createHashtags,
createParticipants,
createReferences,
createHeader,
createDescription,
createAudienceFlags,
createEventMetadata,
createNotePointers,
createSection,
} from "./revertModalRenderers.js";
// NOTE: Any metadata field added to the Upload or Edit modals must also be
@@ -354,7 +361,7 @@ export class RevertModal {
if (this.subtitle) {
const subtitleParts = [];
const dTagValue = this.extractDTagValue(video.tags);
const dTagValue = getDTagValueFromTags(video.tags);
if (dTagValue) {
subtitleParts.push(`d=${dTagValue}`);
}
@@ -588,439 +595,40 @@ export class RevertModal {
return;
}
const createdAt = typeof version.created_at === "number" ? version.created_at : 0;
const absolute = this.formatAbsoluteTimestamp
? this.formatAbsoluteTimestamp(createdAt)
: "";
const relative = this.formatTimeAgo ? this.formatTimeAgo(createdAt) : "";
const description =
typeof version.description === "string" ? version.description : "";
const thumbnail =
typeof version.thumbnail === "string" ? version.thumbnail.trim() : "";
const url = typeof version.url === "string" ? version.url.trim() : "";
const magnet =
typeof version.magnet === "string" ? version.magnet.trim() : "";
const rawMagnet =
typeof version.rawMagnet === "string" ? version.rawMagnet.trim() : "";
const displayMagnet = magnet || rawMagnet;
const isPrivate = version.isPrivate === true;
const dTagValue = this.extractDTagValue(version.tags);
const nip71Metadata = this.buildNip71DisplayMetadata(version);
const dTagValue = getDTagValueFromTags(version.tags);
const fallbackThumbnail = this.fallbackThumbnailSrc;
const thumbnailSrc = thumbnail || fallbackThumbnail;
const thumbnailAlt = thumbnail ? "Revision thumbnail" : "Fallback thumbnail";
const urlNode = url ? this.createLinkMarkup(url) : this.createPlaceholder("None");
const magnetNode = displayMagnet
? (() => {
const div = document.createElement("div");
div.className = "break-all";
const label = this.truncateMiddle
? this.truncateMiddle(displayMagnet, 72)
: displayMagnet;
div.textContent = label;
if (isPrivate) {
const span = document.createElement("span");
span.className = "block mt-1 text-xs text-status-private-on";
span.textContent = "Magnet stays visible only to you — private notes keep the raw string local.";
div.appendChild(span);
}
return div;
})()
: this.createPlaceholder("None");
const chipsDiv = document.createElement("div");
chipsDiv.className = "flex flex-wrap gap-2";
const nsfwFlag = version.isNsfw === true;
const forKidsFlag = version.isForKids === true;
const conflictingAudienceFlags = nsfwFlag && forKidsFlag;
if (version.deleted) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "critical";
s.textContent = "Marked deleted";
chipsDiv.appendChild(s);
}
if (isPrivate) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "private";
s.textContent = "Private";
chipsDiv.appendChild(s);
}
if (conflictingAudienceFlags) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "warning";
s.textContent = "NSFW + For kids conflict";
chipsDiv.appendChild(s);
} else {
if (nsfwFlag) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "critical";
s.textContent = "Marked NSFW";
chipsDiv.appendChild(s);
}
if (forKidsFlag) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "success";
s.textContent = "For kids";
chipsDiv.appendChild(s);
}
}
if (version.version !== undefined) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "neutral";
s.textContent = `Schema v${version.version}`;
chipsDiv.appendChild(s);
}
const createFlagStatus = (value, {yes, no, unspecified}) => {
if (value === true) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = yes;
return s;
}
if (value === false) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = no;
return s;
}
return this.createPlaceholder(unspecified);
};
const nsfwStatusNode = createFlagStatus(version.isNsfw, {
yes: "Yes — marked NSFW",
no: "No — not flagged as NSFW",
unspecified: "Not specified",
});
const kidsStatusNode = createFlagStatus(version.isForKids, {
yes: "Yes — marked for kids",
no: "No — not marked for kids",
unspecified: "Not specified",
});
const audienceConflictNotice = conflictingAudienceFlags
? (() => {
const p = document.createElement("p");
p.className = "text-xs text-status-warning-on";
p.textContent = "Conflicting flags detected — this revision is marked both NSFW and for kids. Review before reverting.";
return p;
})()
: null;
const descriptionNode = description
? (() => {
const p = document.createElement("p");
p.className = "whitespace-pre-wrap text-primary";
p.textContent = description;
return p;
})()
: (() => {
const p = document.createElement("p");
p.className = "text-subtle";
p.textContent = "No description provided.";
return p;
})();
const rootId = typeof version.videoRootId === "string" ? version.videoRootId : "";
const rootDisplay = rootId
? this.truncateMiddle
? this.truncateMiddle(rootId, 64)
: rootId
: "";
const eventDisplay = version.id
? this.truncateMiddle
? this.truncateMiddle(version.id, 64)
: version.id
: "";
const timestampText = [absolute, relative ? `(${relative})` : ""]
.filter(Boolean)
.join(" ");
const createTimestampNode = (seconds, raw) => {
if (Number.isFinite(seconds)) {
const abs = this.formatAbsoluteTimestamp ? this.formatAbsoluteTimestamp(seconds) : `${seconds}`;
const rel = this.formatTimeAgo ? this.formatTimeAgo(seconds) : "";
const frag = document.createDocumentFragment();
if (abs) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = abs;
frag.appendChild(s);
frag.appendChild(document.createTextNode(" "));
}
if (rel) {
const s = document.createElement("span");
s.className = "text-muted";
s.textContent = `(${rel})`;
frag.appendChild(s);
}
return frag;
}
if (raw) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = raw;
return s;
}
return this.createPlaceholder();
};
const createDurationNode = (seconds, raw) => {
if (Number.isFinite(seconds)) {
const display = this.formatDurationSeconds(seconds);
const suffix = seconds === 1 ? "second" : "seconds";
const frag = document.createDocumentFragment();
const s1 = document.createElement("span");
s1.className = "text-primary";
s1.textContent = display;
frag.appendChild(s1);
frag.appendChild(document.createTextNode(" "));
const s2 = document.createElement("span");
s2.className = "text-subtle";
s2.textContent = `(${seconds} ${suffix})`;
frag.appendChild(s2);
return frag;
}
if (raw) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = raw;
return s;
}
return this.createPlaceholder();
};
const kindValue = nip71Metadata.kind;
const kindNode = (() => {
if (kindValue !== "" && kindValue !== null && kindValue !== undefined) {
const label = typeof kindValue === "number" ? kindValue : `${kindValue}`;
const numeric = Number(label);
let suffix = "";
if (Number.isFinite(numeric)) {
if (numeric === 22) suffix = "short";
else if (numeric === 21) suffix = "video";
}
const frag = document.createDocumentFragment();
const code = document.createElement("code");
code.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
code.textContent = `kind ${label}`;
frag.appendChild(code);
if (suffix) {
frag.appendChild(document.createTextNode(" "));
const s = document.createElement("span");
s.className = "text-muted";
s.textContent = `(${suffix})`;
frag.appendChild(s);
}
return frag;
}
return this.createPlaceholder();
})();
const summaryNode = nip71Metadata.summary
? (() => {
const p = document.createElement("p");
p.className = "whitespace-pre-wrap text-primary";
p.textContent = nip71Metadata.summary;
return p;
})()
: this.createPlaceholder("Not provided");
const contentWarningNode = nip71Metadata.contentWarning
? (() => {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "warning";
s.textContent = nip71Metadata.contentWarning;
return s;
})()
: this.createPlaceholder("Not provided");
const publishedAtNode = createTimestampNode(
nip71Metadata.publishedAtSeconds,
nip71Metadata.publishedAtRaw
);
const durationNode = createDurationNode(
nip71Metadata.durationSeconds,
nip71Metadata.durationRaw
);
const altNode = nip71Metadata.alt
? (() => {
const p = document.createElement("p");
p.className = "whitespace-pre-wrap text-primary";
p.textContent = nip71Metadata.alt;
return p;
})()
: this.createPlaceholder("Not provided");
const createSection = (title) => {
const s = document.createElement("section");
s.className = "space-y-2";
const h = document.createElement("h4");
h.className = "text-sm font-semibold text-primary";
h.textContent = title;
s.appendChild(h);
return s;
};
const createDl = () => {
const dl = document.createElement("dl");
dl.className = "grid gap-3 sm:grid-cols-2 text-xs text-subtle";
return dl;
};
const createDtDd = (label, contentNode, colSpan = 1) => {
const div = document.createElement("div");
if (colSpan > 1) div.className = "sm:col-span-2";
const dt = document.createElement("dt");
dt.className = "font-semibold text-primary";
dt.textContent = label;
div.appendChild(dt);
const dd = document.createElement("dd");
dd.className = "mt-1";
dd.appendChild(contentNode);
div.appendChild(dd);
return div;
const utils = {
formatAbsoluteTimestamp: this.formatAbsoluteTimestamp,
formatTimeAgo: this.formatTimeAgo,
truncateMiddle: this.truncateMiddle,
createPlaceholder: this.createPlaceholder.bind(this),
createLinkMarkup: this.createLinkMarkup.bind(this),
createListEmpty: this.createListEmpty.bind(this),
formatDurationSeconds: this.formatDurationSeconds.bind(this),
fallbackThumbnailSrc: this.fallbackThumbnailSrc,
};
const wrapper = document.createElement("div");
wrapper.className = "space-y-6";
// Header Area
const headerRow = document.createElement("div");
headerRow.className = "flex flex-col gap-4 lg:flex-row lg:items-start";
const imgDiv = document.createElement("div");
imgDiv.className = "overflow-hidden rounded-md border border-overlay bg-overlay-strong w-full max-w-sm";
const img = document.createElement("img");
img.src = thumbnailSrc;
img.alt = thumbnailAlt;
img.className = "w-full h-auto object-cover";
img.loading = "lazy";
imgDiv.appendChild(img);
headerRow.appendChild(imgDiv);
const headerContent = document.createElement("div");
headerContent.className = "flex-1 space-y-3";
const titleTimeDiv = document.createElement("div");
titleTimeDiv.className = "space-y-1";
const titleH3 = document.createElement("h3");
titleH3.className = "text-lg font-semibold text-primary";
titleH3.textContent = version.title || "Untitled";
titleTimeDiv.appendChild(titleH3);
const timeP = document.createElement("p");
timeP.className = "text-xs text-muted";
timeP.textContent = timestampText;
titleTimeDiv.appendChild(timeP);
headerContent.appendChild(titleTimeDiv);
if (chipsDiv.hasChildNodes()) {
headerContent.appendChild(chipsDiv);
}
const headerDl = createDl();
headerDl.appendChild(createDtDd("Hosted URL", urlNode));
headerDl.appendChild(createDtDd("Magnet", magnetNode));
headerContent.appendChild(headerDl);
headerRow.appendChild(headerContent);
wrapper.appendChild(headerRow);
wrapper.appendChild(createHeader(version, nip71Metadata, utils));
// Description
const descSection = createSection("Description");
descSection.appendChild(descriptionNode);
wrapper.appendChild(descSection);
wrapper.appendChild(createDescription(
typeof version.description === "string" ? version.description : "",
utils
));
// Audience Flags
const audSection = createSection("Audience flags");
const audDl = createDl();
audDl.appendChild(createDtDd("NSFW flag", nsfwStatusNode));
audDl.appendChild(createDtDd("For kids flag", kidsStatusNode));
audSection.appendChild(audDl);
if (audienceConflictNotice) audSection.appendChild(audienceConflictNotice);
wrapper.appendChild(audSection);
wrapper.appendChild(createAudienceFlags(version, utils));
// NIP-71 event metadata
const eventSection = createSection("NIP-71 event metadata");
const eventDl = createDl();
eventDl.appendChild(createDtDd("NIP-71 kind", kindNode));
eventDl.appendChild(createDtDd("Summary", summaryNode, 2));
eventDl.appendChild(createDtDd("Content warning", contentWarningNode, 2));
eventDl.appendChild(createDtDd("Published timestamp", publishedAtNode));
eventDl.appendChild(createDtDd("Duration", durationNode));
eventDl.appendChild(createDtDd("Alt text", altNode, 2));
eventSection.appendChild(eventDl);
wrapper.appendChild(eventSection);
wrapper.appendChild(createEventMetadata(nip71Metadata, utils));
// Note pointers
const ptrSection = createSection("Note pointers");
const ptrDl = createDl();
ptrDl.appendChild(createDtDd("Mode", document.createTextNode(version.mode || "live")));
const dTagNode = dTagValue
? (() => {
const c = document.createElement("code");
c.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
c.textContent = dTagValue;
return c;
})()
: this.createPlaceholder("Not provided");
ptrDl.appendChild(createDtDd("d tag", dTagNode));
const rootIdNode = rootDisplay
? (() => {
const c = document.createElement("code");
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
c.title = rootId;
c.textContent = rootDisplay;
return c;
})()
: this.createPlaceholder("Not provided");
ptrDl.appendChild(createDtDd("videoRootId", rootIdNode));
const eventIdNode = eventDisplay
? (() => {
const c = document.createElement("code");
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
c.title = version.id || "";
c.textContent = eventDisplay;
return c;
})()
: (() => {
const s = document.createElement("span");
s.className = "text-subtle";
s.textContent = "Unknown";
return s;
})();
ptrDl.appendChild(createDtDd("Event ID", eventIdNode));
ptrSection.appendChild(ptrDl);
wrapper.appendChild(ptrSection);
wrapper.appendChild(createNotePointers(version, dTagValue, utils));
// NIP-71 media metadata
const mediaSection = createSection("NIP-71 media metadata");
@@ -1038,20 +646,12 @@ export class RevertModal {
return d;
};
const renderUtils = {
createListEmpty: (t) => this.createListEmpty(t),
createPlaceholder: (t) => this.createPlaceholder(t),
createLinkMarkup: (v, o) => this.createLinkMarkup(v, o),
formatDurationSeconds: (s) => this.formatDurationSeconds(s),
truncateMiddle: this.truncateMiddle,
};
mediaDiv.appendChild(createMediaSub("Media variants (imeta)", createImetaVariants(nip71Metadata.imeta, renderUtils)));
mediaDiv.appendChild(createMediaSub("Caption tracks", createTextTracks(nip71Metadata.textTracks, renderUtils)));
mediaDiv.appendChild(createMediaSub("Chapters", createSegments(nip71Metadata.segments, renderUtils)));
mediaDiv.appendChild(createMediaSub("Hashtags", createHashtags(nip71Metadata.hashtags, renderUtils)));
mediaDiv.appendChild(createMediaSub("Participants", createParticipants(nip71Metadata.participants, renderUtils)));
mediaDiv.appendChild(createMediaSub("References", createReferences(nip71Metadata.references, renderUtils)));
mediaDiv.appendChild(createMediaSub("Media variants (imeta)", createImetaVariants(nip71Metadata.imeta, utils)));
mediaDiv.appendChild(createMediaSub("Caption tracks", createTextTracks(nip71Metadata.textTracks, utils)));
mediaDiv.appendChild(createMediaSub("Chapters", createSegments(nip71Metadata.segments, utils)));
mediaDiv.appendChild(createMediaSub("Hashtags", createHashtags(nip71Metadata.hashtags, utils)));
mediaDiv.appendChild(createMediaSub("Participants", createParticipants(nip71Metadata.participants, utils)));
mediaDiv.appendChild(createMediaSub("References", createReferences(nip71Metadata.references, utils)));
mediaSection.appendChild(mediaDiv);
wrapper.appendChild(mediaSection);
@@ -1488,21 +1088,4 @@ export class RevertModal {
references,
};
}
extractDTagValue(tags) {
if (!Array.isArray(tags)) {
return "";
}
for (const tag of tags) {
if (!Array.isArray(tag) || tag.length < 2) {
continue;
}
if (tag[0] === "d" && typeof tag[1] === "string") {
return tag[1];
}
}
return "";
}
}
+456
View File
@@ -437,3 +437,459 @@ export function createReferences(references, utils) {
return ul;
}
/* Shared Helpers (internal to this module, or exported if needed elsewhere) */
function createDl() {
const dl = document.createElement("dl");
dl.className = "grid gap-3 sm:grid-cols-2 text-xs text-subtle";
return dl;
}
function createDtDd(label, contentNode, colSpan = 1) {
const div = document.createElement("div");
if (colSpan > 1) div.className = "sm:col-span-2";
const dt = document.createElement("dt");
dt.className = "font-semibold text-primary";
dt.textContent = label;
div.appendChild(dt);
const dd = document.createElement("dd");
dd.className = "mt-1";
dd.appendChild(contentNode);
div.appendChild(dd);
return div;
}
export function createSection(title) {
const s = document.createElement("section");
s.className = "space-y-2";
const h = document.createElement("h4");
h.className = "text-sm font-semibold text-primary";
h.textContent = title;
s.appendChild(h);
return s;
}
function createTimestampNode(seconds, raw, utils) {
const { formatAbsoluteTimestamp, formatTimeAgo, createPlaceholder } = utils;
if (Number.isFinite(seconds)) {
const abs = formatAbsoluteTimestamp ? formatAbsoluteTimestamp(seconds) : `${seconds}`;
const rel = formatTimeAgo ? formatTimeAgo(seconds) : "";
const frag = document.createDocumentFragment();
if (abs) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = abs;
frag.appendChild(s);
frag.appendChild(document.createTextNode(" "));
}
if (rel) {
const s = document.createElement("span");
s.className = "text-muted";
s.textContent = `(${rel})`;
frag.appendChild(s);
}
return frag;
}
if (raw) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = raw;
return s;
}
return createPlaceholder();
}
function createDurationNode(seconds, raw, utils) {
const { formatDurationSeconds, createPlaceholder } = utils;
if (Number.isFinite(seconds)) {
const display = formatDurationSeconds ? formatDurationSeconds(seconds) : `${seconds}`;
const suffix = seconds === 1 ? "second" : "seconds";
const frag = document.createDocumentFragment();
const s1 = document.createElement("span");
s1.className = "text-primary";
s1.textContent = display;
frag.appendChild(s1);
frag.appendChild(document.createTextNode(" "));
const s2 = document.createElement("span");
s2.className = "text-subtle";
s2.textContent = `(${seconds} ${suffix})`;
frag.appendChild(s2);
return frag;
}
if (raw) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = raw;
return s;
}
return createPlaceholder();
}
function createFlagStatus(value, { yes, no, unspecified }, utils) {
const { createPlaceholder } = utils;
if (value === true) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = yes;
return s;
}
if (value === false) {
const s = document.createElement("span");
s.className = "text-primary";
s.textContent = no;
return s;
}
return createPlaceholder(unspecified);
}
/* New Exported Renderers */
export function createHeader(version, nip71Metadata, utils) {
const {
formatAbsoluteTimestamp,
formatTimeAgo,
truncateMiddle,
createPlaceholder,
createLinkMarkup,
fallbackThumbnailSrc,
} = utils;
const wrapper = document.createElement("div");
wrapper.className = "space-y-6";
const createdAt = typeof version.created_at === "number" ? version.created_at : 0;
const absolute = formatAbsoluteTimestamp ? formatAbsoluteTimestamp(createdAt) : "";
const relative = formatTimeAgo ? formatTimeAgo(createdAt) : "";
const timestampText = [absolute, relative ? `(${relative})` : ""].filter(Boolean).join(" ");
const thumbnail = typeof version.thumbnail === "string" ? version.thumbnail.trim() : "";
const thumbnailSrc = thumbnail || fallbackThumbnailSrc;
const thumbnailAlt = thumbnail ? "Revision thumbnail" : "Fallback thumbnail";
const url = typeof version.url === "string" ? version.url.trim() : "";
const urlNode = url ? createLinkMarkup(url) : createPlaceholder("None");
const magnet = typeof version.magnet === "string" ? version.magnet.trim() : "";
const rawMagnet = typeof version.rawMagnet === "string" ? version.rawMagnet.trim() : "";
const displayMagnet = magnet || rawMagnet;
const isPrivate = version.isPrivate === true;
const magnetNode = displayMagnet
? (() => {
const div = document.createElement("div");
div.className = "break-all";
const label = truncateMiddle ? truncateMiddle(displayMagnet, 72) : displayMagnet;
div.textContent = label;
if (isPrivate) {
const span = document.createElement("span");
span.className = "block mt-1 text-xs text-status-private-on";
span.textContent = "Magnet stays visible only to you — private notes keep the raw string local.";
div.appendChild(span);
}
return div;
})()
: createPlaceholder("None");
// Chips
const chipsDiv = document.createElement("div");
chipsDiv.className = "flex flex-wrap gap-2";
const nsfwFlag = version.isNsfw === true;
const forKidsFlag = version.isForKids === true;
const conflictingAudienceFlags = nsfwFlag && forKidsFlag;
if (version.deleted) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "critical";
s.textContent = "Marked deleted";
chipsDiv.appendChild(s);
}
if (isPrivate) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "private";
s.textContent = "Private";
chipsDiv.appendChild(s);
}
if (conflictingAudienceFlags) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "warning";
s.textContent = "NSFW + For kids conflict";
chipsDiv.appendChild(s);
} else {
if (nsfwFlag) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "critical";
s.textContent = "Marked NSFW";
chipsDiv.appendChild(s);
}
if (forKidsFlag) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "success";
s.textContent = "For kids";
chipsDiv.appendChild(s);
}
}
if (version.version !== undefined) {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "neutral";
s.textContent = `Schema v${version.version}`;
chipsDiv.appendChild(s);
}
// DOM construction
const headerRow = document.createElement("div");
headerRow.className = "flex flex-col gap-4 lg:flex-row lg:items-start";
const imgDiv = document.createElement("div");
imgDiv.className = "overflow-hidden rounded-md border border-overlay bg-overlay-strong w-full max-w-sm";
const img = document.createElement("img");
img.src = thumbnailSrc;
img.alt = thumbnailAlt;
img.className = "w-full h-auto object-cover";
img.loading = "lazy";
imgDiv.appendChild(img);
headerRow.appendChild(imgDiv);
const headerContent = document.createElement("div");
headerContent.className = "flex-1 space-y-3";
const titleTimeDiv = document.createElement("div");
titleTimeDiv.className = "space-y-1";
const titleH3 = document.createElement("h3");
titleH3.className = "text-lg font-semibold text-primary";
titleH3.textContent = version.title || "Untitled";
titleTimeDiv.appendChild(titleH3);
const timeP = document.createElement("p");
timeP.className = "text-xs text-muted";
timeP.textContent = timestampText;
titleTimeDiv.appendChild(timeP);
headerContent.appendChild(titleTimeDiv);
if (chipsDiv.hasChildNodes()) {
headerContent.appendChild(chipsDiv);
}
const headerDl = createDl();
headerDl.appendChild(createDtDd("Hosted URL", urlNode));
headerDl.appendChild(createDtDd("Magnet", magnetNode));
headerContent.appendChild(headerDl);
headerRow.appendChild(headerContent);
wrapper.appendChild(headerRow);
return wrapper;
}
export function createDescription(description, utils) {
const descSection = createSection("Description");
const descriptionNode = description
? (() => {
const p = document.createElement("p");
p.className = "whitespace-pre-wrap text-primary";
p.textContent = description;
return p;
})()
: (() => {
const p = document.createElement("p");
p.className = "text-subtle";
p.textContent = "No description provided.";
return p;
})();
descSection.appendChild(descriptionNode);
return descSection;
}
export function createAudienceFlags(version, utils) {
const audSection = createSection("Audience flags");
const audDl = createDl();
const nsfwStatusNode = createFlagStatus(version.isNsfw, {
yes: "Yes — marked NSFW",
no: "No — not flagged as NSFW",
unspecified: "Not specified",
}, utils);
const kidsStatusNode = createFlagStatus(version.isForKids, {
yes: "Yes — marked for kids",
no: "No — not marked for kids",
unspecified: "Not specified",
}, utils);
audDl.appendChild(createDtDd("NSFW flag", nsfwStatusNode));
audDl.appendChild(createDtDd("For kids flag", kidsStatusNode));
audSection.appendChild(audDl);
if (version.isNsfw === true && version.isForKids === true) {
const p = document.createElement("p");
p.className = "text-xs text-status-warning-on";
p.textContent = "Conflicting flags detected — this revision is marked both NSFW and for kids. Review before reverting.";
audSection.appendChild(p);
}
return audSection;
}
export function createEventMetadata(nip71Metadata, utils) {
const { createPlaceholder } = utils;
const eventSection = createSection("NIP-71 event metadata");
const eventDl = createDl();
const kindValue = nip71Metadata.kind;
const kindNode = (() => {
if (kindValue !== "" && kindValue !== null && kindValue !== undefined) {
const label = typeof kindValue === "number" ? kindValue : `${kindValue}`;
const numeric = Number(label);
let suffix = "";
if (Number.isFinite(numeric)) {
if (numeric === 22) suffix = "short";
else if (numeric === 21) suffix = "video";
}
const frag = document.createDocumentFragment();
const code = document.createElement("code");
code.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
code.textContent = `kind ${label}`;
frag.appendChild(code);
if (suffix) {
frag.appendChild(document.createTextNode(" "));
const s = document.createElement("span");
s.className = "text-muted";
s.textContent = `(${suffix})`;
frag.appendChild(s);
}
return frag;
}
return createPlaceholder();
})();
eventDl.appendChild(createDtDd("NIP-71 kind", kindNode));
const summaryNode = nip71Metadata.summary
? (() => {
const p = document.createElement("p");
p.className = "whitespace-pre-wrap text-primary";
p.textContent = nip71Metadata.summary;
return p;
})()
: createPlaceholder("Not provided");
eventDl.appendChild(createDtDd("Summary", summaryNode, 2));
const contentWarningNode = nip71Metadata.contentWarning
? (() => {
const s = document.createElement("span");
s.className = "pill";
s.dataset.size = "compact";
s.dataset.variant = "warning";
s.textContent = nip71Metadata.contentWarning;
return s;
})()
: createPlaceholder("Not provided");
eventDl.appendChild(createDtDd("Content warning", contentWarningNode, 2));
const publishedAtNode = createTimestampNode(
nip71Metadata.publishedAtSeconds,
nip71Metadata.publishedAtRaw,
utils
);
eventDl.appendChild(createDtDd("Published timestamp", publishedAtNode));
const durationNode = createDurationNode(
nip71Metadata.durationSeconds,
nip71Metadata.durationRaw,
utils
);
eventDl.appendChild(createDtDd("Duration", durationNode));
const altNode = nip71Metadata.alt
? (() => {
const p = document.createElement("p");
p.className = "whitespace-pre-wrap text-primary";
p.textContent = nip71Metadata.alt;
return p;
})()
: createPlaceholder("Not provided");
eventDl.appendChild(createDtDd("Alt text", altNode, 2));
eventSection.appendChild(eventDl);
return eventSection;
}
export function createNotePointers(version, dTagValue, utils) {
const { createPlaceholder, truncateMiddle } = utils;
const ptrSection = createSection("Note pointers");
const ptrDl = createDl();
ptrDl.appendChild(createDtDd("Mode", document.createTextNode(version.mode || "live")));
const dTagNode = dTagValue
? (() => {
const c = document.createElement("code");
c.className = "rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
c.textContent = dTagValue;
return c;
})()
: createPlaceholder("Not provided");
ptrDl.appendChild(createDtDd("d tag", dTagNode));
const rootId = typeof version.videoRootId === "string" ? version.videoRootId : "";
const rootDisplay = rootId
? truncateMiddle
? truncateMiddle(rootId, 64)
: rootId
: "";
const rootIdNode = rootDisplay
? (() => {
const c = document.createElement("code");
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
c.title = rootId;
c.textContent = rootDisplay;
return c;
})()
: createPlaceholder("Not provided");
ptrDl.appendChild(createDtDd("videoRootId", rootIdNode));
const eventDisplay = version.id
? truncateMiddle
? truncateMiddle(version.id, 64)
: version.id
: "";
const eventIdNode = eventDisplay
? (() => {
const c = document.createElement("code");
c.className = "break-all rounded bg-overlay-panel-soft px-1.5 py-0.5 text-primary";
c.title = version.id || "";
c.textContent = eventDisplay;
return c;
})()
: (() => {
const s = document.createElement("span");
s.className = "text-subtle";
s.textContent = "Unknown";
return s;
})();
ptrDl.appendChild(createDtDd("Event ID", eventIdNode));
ptrSection.appendChild(ptrDl);
return ptrSection;
}
@@ -0,0 +1,208 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
createHeader,
createDescription,
createAudienceFlags,
createEventMetadata,
createNotePointers,
} from '../../../js/ui/components/revertModalRenderers.js';
// --- Mocks ---
class MockHTMLElement {
constructor(tagName) {
this.tagName = tagName.toUpperCase();
this.className = '';
this.textContent = '';
this.children = [];
this.attributes = {};
this.dataset = {};
}
appendChild(child) {
if (typeof child === 'string') {
// Text node simulation
this.children.push({ nodeType: 3, textContent: child });
} else {
this.children.push(child);
}
return child;
}
setAttribute(name, value) {
this.attributes[name] = value;
}
hasChildNodes() {
return this.children.length > 0;
}
}
const documentMock = {
createElement: (tagName) => new MockHTMLElement(tagName),
createDocumentFragment: () => {
const fragment = {
nodeType: 11,
children: [],
appendChild(child) {
this.children.push(child);
}
};
return fragment;
},
createTextNode: (text) => ({ nodeType: 3, textContent: text }),
};
// Global document mock
globalThis.document = documentMock;
const utilsMock = {
formatAbsoluteTimestamp: (ts) => `ABS:${ts}`,
formatTimeAgo: (ts) => `AGO:${ts}`,
truncateMiddle: (str) => `TRUNC:${str}`,
createPlaceholder: (text) => {
const s = new MockHTMLElement('span');
s.textContent = text || 'PLACEHOLDER';
return s;
},
createLinkMarkup: (url) => {
const a = new MockHTMLElement('a');
a.attributes.href = url;
a.textContent = url;
return a;
},
createListEmpty: (text) => {
const p = new MockHTMLElement('p');
p.textContent = text;
return p;
},
formatDurationSeconds: (s) => `DUR:${s}`,
fallbackThumbnailSrc: 'fallback.jpg',
};
// --- Tests ---
test('createHeader renders correctly', () => {
const version = {
created_at: 1234567890,
title: 'Test Title',
thumbnail: 'thumb.jpg',
url: 'https://example.com',
magnet: 'magnet:?xt=urn:btih:123',
isNsfw: true,
version: 2
};
const nip71Metadata = {};
const header = createHeader(version, nip71Metadata, utilsMock);
assert.equal(header.tagName, 'DIV');
assert.ok(header.className.includes('space-y-6'));
// Find title
const h3 = findElement(header, 'H3');
assert.equal(h3.textContent, 'Test Title');
// Find timestamp
// We expect "ABS:1234567890 (AGO:1234567890)"
const p = findElement(header, 'P', (el) => el.textContent.includes('ABS:'));
assert.ok(p);
// Find NSFW pill
const nsfwPill = findElement(header, 'SPAN', (el) => el.textContent === 'Marked NSFW');
assert.ok(nsfwPill);
// Find Schema pill
const schemaPill = findElement(header, 'SPAN', (el) => el.textContent === 'Schema v2');
assert.ok(schemaPill);
});
test('createDescription renders description', () => {
const desc = "My Description";
const section = createDescription(desc, utilsMock);
assert.equal(section.tagName, 'SECTION');
// Check title (from createSection mock)
const h4 = findElement(section, 'H4');
assert.equal(h4.textContent, 'Description');
const p = findElement(section, 'P', (el) => el.textContent === desc);
assert.ok(p);
});
test('createAudienceFlags renders flags', () => {
const version = { isNsfw: true, isForKids: false };
const section = createAudienceFlags(version, utilsMock);
const nsfw = findElement(section, 'SPAN', (el) => el.textContent === 'Yes — marked NSFW');
assert.ok(nsfw);
const kids = findElement(section, 'SPAN', (el) => el.textContent === 'No — not marked for kids');
assert.ok(kids);
});
test('createEventMetadata renders metadata', () => {
const metadata = {
kind: 21,
summary: 'Summary Text',
contentWarning: 'Warning',
publishedAtSeconds: 1000,
durationSeconds: 60,
alt: 'Alt Text'
};
const section = createEventMetadata(metadata, utilsMock);
// Kind
const kindCode = findElement(section, 'CODE', (el) => el.textContent === 'kind 21');
assert.ok(kindCode);
// Summary
const summary = findElement(section, 'P', (el) => el.textContent === 'Summary Text');
assert.ok(summary);
// Duration
const duration = findElement(section, 'SPAN', (el) => el.textContent === 'DUR:60');
assert.ok(duration);
});
test('createNotePointers renders pointers', () => {
const version = {
mode: 'live',
videoRootId: 'root123',
id: 'event123'
};
const dTag = 'd-tag-val';
const section = createNotePointers(version, dTag, utilsMock);
const dTagCode = findElement(section, 'CODE', (el) => el.textContent === 'd-tag-val');
assert.ok(dTagCode);
const rootCode = findElement(section, 'CODE', (el) => el.textContent === 'TRUNC:root123');
assert.ok(rootCode);
});
// Helper to recursively find element
function findElement(root, tagName, predicate) {
if (!root || !root.children) return null;
if (root.tagName === tagName) {
if (!predicate || predicate(root)) return root;
}
for (const child of root.children) {
if (child.nodeType === 3) continue; // Skip text nodes
if (child.nodeType === 11) { // Fragment
const found = findElement(child, tagName, predicate);
if (found) return found;
continue;
}
const found = findElement(child, tagName, predicate);
if (found) return found;
}
return null;
}