From 2703382ab452b4b92f242721df212738fea2238b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:50:10 +0000 Subject: [PATCH] Refactor RevertModal rendering logic into separate module Extracted excessive DOM rendering logic from `RevertModal.js` into `revertModalRenderers.js`. This improves code readability and maintainability by separating the view construction from the modal logic. Added unit tests for the new renderers using a lightweight DOM mock. Replaced local d-tag extraction with shared utility `getDTagValueFromTags`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/ui/components/RevertModal.js | 481 ++---------------- js/ui/components/revertModalRenderers.js | 456 +++++++++++++++++ .../components/revertModalRenderers.test.mjs | 208 ++++++++ 3 files changed, 696 insertions(+), 449 deletions(-) create mode 100644 tests/ui/components/revertModalRenderers.test.mjs diff --git a/js/ui/components/RevertModal.js b/js/ui/components/RevertModal.js index 3e2d4ab0..f53ed0c0 100644 --- a/js/ui/components/RevertModal.js +++ b/js/ui/components/RevertModal.js @@ -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 ""; - } } - diff --git a/js/ui/components/revertModalRenderers.js b/js/ui/components/revertModalRenderers.js index 1f8bd553..06edca3d 100644 --- a/js/ui/components/revertModalRenderers.js +++ b/js/ui/components/revertModalRenderers.js @@ -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; +} diff --git a/tests/ui/components/revertModalRenderers.test.mjs b/tests/ui/components/revertModalRenderers.test.mjs new file mode 100644 index 00000000..d9cb45a7 --- /dev/null +++ b/tests/ui/components/revertModalRenderers.test.mjs @@ -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; +}