From ff83438dee8abc186876e8825b649b84335d9dd9 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:02:09 -0400 Subject: [PATCH] Enhance revert modal metadata details --- components/revert-video-modal.html | 60 ++- js/ui/components/RevertModal.js | 815 ++++++++++++++++++++++++++--- 2 files changed, 805 insertions(+), 70 deletions(-) diff --git a/components/revert-video-modal.html b/components/revert-video-modal.html index 87f2e6d7..cedd0daa 100644 --- a/components/revert-video-modal.html +++ b/components/revert-video-modal.html @@ -87,8 +87,64 @@ class="rounded-lg border border-gray-800 bg-gray-900/70 p-4 text-sm text-gray-200" aria-live="polite" > -
+ Select an earlier revision to inspect its metadata. +
++ Media variants, text tracks, chapters, hashtags, participants, and references will appear once a revision is selected. +
+${escape( - description - )}
` + ? `${escape(description)}
` : 'No description provided.
'; const rootId = @@ -605,8 +606,120 @@ export class RevertModal { : escape(version.id) : ""; + const timestampParts = []; + if (absolute) { + timestampParts.push(escape(absolute)); + } + if (relative) { + timestampParts.push(`(${escape(relative)})`); + } + const timestampHtml = timestampParts.length + ? timestampParts.join(" ") + : escape(absolute || relative || ""); + + const formatTimestamp = (seconds, raw) => { + if (Number.isFinite(seconds)) { + const absoluteLabel = this.formatAbsoluteTimestamp + ? this.formatAbsoluteTimestamp(seconds) + : `${seconds}`; + const relativeLabel = this.formatTimeAgo + ? this.formatTimeAgo(seconds) + : ""; + const parts = []; + if (absoluteLabel) { + parts.push(`${escape(absoluteLabel)}`); + } + if (relativeLabel) { + parts.push(`(${escape(relativeLabel)})`); + } + if (parts.length) { + return parts.join(" "); + } + } + if (raw) { + return `${escape(raw)}`; + } + return this.renderPlaceholder(); + }; + + const formatDuration = (seconds, raw) => { + if (Number.isFinite(seconds)) { + const display = this.formatDurationSeconds(seconds); + const suffix = seconds === 1 ? "second" : "seconds"; + return `${escape(display)} (${escape( + `${seconds} ${suffix}` + )})`; + } + if (raw) { + return `${escape(raw)}`; + } + return this.renderPlaceholder(); + }; + + const kindValue = nip71Metadata.kind; + let kindHtml = this.renderPlaceholder(); + 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 badge = `kind ${escape(
+ label
+ )}`;
+ kindHtml = suffix
+ ? `${badge} (${escape(suffix)})`
+ : badge;
+ }
+
+ const summaryHtml = nip71Metadata.summary
+ ? `${escape( + nip71Metadata.summary + )}
` + : this.renderPlaceholder("Not provided"); + const contentWarningHtml = nip71Metadata.contentWarning + ? `${escape( + nip71Metadata.contentWarning + )}` + : this.renderPlaceholder("Not provided"); + const publishedAtHtml = formatTimestamp( + nip71Metadata.publishedAtSeconds, + nip71Metadata.publishedAtRaw + ); + const durationHtml = formatDuration( + nip71Metadata.durationSeconds, + nip71Metadata.durationRaw + ); + const altHtml = nip71Metadata.alt + ? `${escape(nip71Metadata.alt)}
` + : this.renderPlaceholder("Not provided"); + + const definition = (label, valueHtml, { span = 1 } = {}) => { + const colSpan = span > 1 ? "sm:col-span-2" : ""; + return ` +${escape(absolute)} (${escape( - relative - )})
+${timestampHtml}
${escape(
+ dTagValue
+ )}`
+ : 'Not provided'
+ }
+ ${rootDisplay}`
+ : 'Not provided'
+ }
+ ${eventDisplay}`
+ : 'Unknown'
+ }
+ ${escape(
- dTagValue
- )}`
- : 'Not provided'
- }
- ${rootDisplay}`
- : 'Not provided'
- }
- ${eventDisplay}`
- : 'Unknown'
- }
- ${escape(text)}
`; + } + + buildLinkMarkup(value, { breakAll = true } = {}) { + if (value == null) { + return ""; + } + + const escape = this.getEscapeFn(); + const raw = typeof value === "string" ? value.trim() : String(value ?? "").trim(); + if (!raw) { + return ""; + } + + const displayRaw = this.truncateMiddle + ? this.truncateMiddle(raw, 96) + : raw; + const safeDisplay = escape(displayRaw); + const safeValue = escape(raw); + const breakClass = breakAll ? " break-all" : " break-words"; + + if (/^https?:/i.test(raw)) { + return `${safeDisplay}`; + } + + return `${safeDisplay}`;
+ }
+
+ formatDurationSeconds(seconds) {
+ if (!Number.isFinite(seconds)) {
+ return "";
+ }
+
+ const total = Math.max(0, Math.floor(seconds));
+ const hours = Math.floor(total / 3600);
+ const minutes = Math.floor((total % 3600) / 60);
+ const secs = total % 60;
+
+ if (hours > 0) {
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
+ }
+
+ return `${minutes}:${String(secs).padStart(2, "0")}`;
+ }
+
+ buildNip71DisplayMetadata(version) {
+ const raw =
+ version && typeof version === "object" && version.nip71 && typeof version.nip71 === "object"
+ ? version.nip71
+ : {};
+
+ const { normalizeNumber, toInputValue } = this.nip71Formatter || {};
+
+ const toString = (value) => {
+ if (value == null) {
+ return "";
+ }
+ if (typeof value === "string") {
+ return value.trim();
+ }
+ if (typeof value === "number" || typeof value === "boolean") {
+ const converted = toInputValue ? toInputValue(value) : `${value}`;
+ return typeof converted === "string" ? converted.trim() : "";
+ }
+ return "";
+ };
+
+ const parseNumeric = (value) => {
+ if (value == null) {
+ return null;
+ }
+ if (typeof value === "string" && value.includes(":")) {
+ return null;
+ }
+ if (normalizeNumber) {
+ const parsed = normalizeNumber(value);
+ if (typeof parsed === "number" && Number.isFinite(parsed)) {
+ return parsed;
+ }
+ }
+ if (typeof value === "number" && Number.isFinite(value)) {
+ return value;
+ }
+ if (typeof value === "string") {
+ const trimmed = value.trim();
+ if (!trimmed) {
+ return null;
+ }
+ const numeric = Number(trimmed);
+ return Number.isFinite(numeric) ? numeric : null;
+ }
+ return null;
+ };
+
+ const parseTimestamp = (value) => {
+ const rawValue = toString(value);
+ if (!rawValue) {
+ return { raw: "", seconds: null };
+ }
+
+ if (/^\d+(?:\.\d+)?$/.test(rawValue)) {
+ const numeric = Number(rawValue);
+ if (Number.isFinite(numeric)) {
+ const normalized = numeric > 1e12 ? Math.floor(numeric / 1000) : Math.floor(numeric);
+ return { raw: rawValue, seconds: normalized };
+ }
+ }
+
+ const parsed = Date.parse(rawValue);
+ if (!Number.isNaN(parsed)) {
+ return { raw: rawValue, seconds: Math.floor(parsed / 1000) };
+ }
+
+ return { raw: rawValue, seconds: null };
+ };
+
+ const collectStrings = (values) => {
+ if (!Array.isArray(values)) {
+ return [];
+ }
+ return values
+ .map((entry) => toString(entry))
+ .map((entry) => entry.trim())
+ .filter((entry) => Boolean(entry));
+ };
+
+ const collectObjects = (values, mapFn) => {
+ if (!Array.isArray(values)) {
+ return [];
+ }
+ return values
+ .map((entry) => (entry && typeof entry === "object" ? mapFn(entry) : null))
+ .filter((entry) => entry);
+ };
+
+ const kindValue = parseNumeric(raw.kind);
+ const publishedAt = parseTimestamp(
+ raw.publishedAt ?? raw.published_at ?? raw["published-at"]
+ );
+
+ const durationNumeric = parseNumeric(raw.duration);
+ const durationRaw = durationNumeric != null ? `${durationNumeric}` : toString(raw.duration);
+
+ const imeta = collectObjects(raw.imeta, (variant) => {
+ const mapped = {
+ m: toString(variant.m),
+ dim: toString(variant.dim),
+ url: toString(variant.url),
+ x: toString(variant.x),
+ image: collectStrings(variant.image),
+ fallback: collectStrings(variant.fallback),
+ service: collectStrings(variant.service),
+ autoGenerated: variant.autoGenerated === true,
+ };
+
+ const hasContent =
+ mapped.m ||
+ mapped.dim ||
+ mapped.url ||
+ mapped.x ||
+ mapped.image.length ||
+ mapped.fallback.length ||
+ mapped.service.length;
+
+ return hasContent ? mapped : null;
+ });
+
+ const textTracks = collectObjects(raw.textTracks, (track) => {
+ const mapped = {
+ url: toString(track.url),
+ type: toString(track.type),
+ language: toString(track.language),
+ };
+ return mapped.url || mapped.type || mapped.language ? mapped : null;
+ });
+
+ const segments = collectObjects(raw.segments, (segment) => {
+ const startNumeric = parseNumeric(segment.start);
+ const endNumeric = parseNumeric(segment.end);
+ const mapped = {
+ startSeconds: startNumeric != null ? Math.max(0, startNumeric) : null,
+ endSeconds: endNumeric != null ? Math.max(0, endNumeric) : null,
+ startRaw: toString(segment.start),
+ endRaw: toString(segment.end),
+ title: toString(segment.title),
+ thumbnail: toString(segment.thumbnail),
+ };
+ return mapped.startRaw || mapped.endRaw || mapped.title || mapped.thumbnail
+ ? mapped
+ : null;
+ });
+
+ const hashtags = collectStrings(raw.hashtags);
+ const participants = collectObjects(raw.participants, (participant) => {
+ const mapped = {
+ pubkey: toString(participant.pubkey),
+ relay: toString(participant.relay),
+ };
+ return mapped.pubkey || mapped.relay ? mapped : null;
+ });
+ const references = collectStrings(raw.references);
+
+ return {
+ kind: kindValue != null ? kindValue : toString(raw.kind),
+ summary: toString(raw.summary),
+ contentWarning:
+ toString(raw.contentWarning ?? raw["content-warning"] ?? raw.content_warning),
+ publishedAtSeconds: publishedAt.seconds,
+ publishedAtRaw: publishedAt.raw,
+ durationSeconds: durationNumeric != null ? Math.max(0, durationNumeric) : null,
+ durationRaw,
+ alt: toString(raw.alt),
+ imeta,
+ textTracks,
+ segments,
+ hashtags,
+ participants,
+ references,
+ };
+ }
+
+ renderImetaVariants(variants) {
+ if (!Array.isArray(variants) || variants.length === 0) {
+ return this.renderListEmpty("No media variants provided.");
+ }
+
+ const escape = this.getEscapeFn();
+
+ return `${escape(
+ variant.m
+ )}`
+ : this.renderPlaceholder();
+ const dimHtml = variant.dim
+ ? `${escape(
+ variant.dim
+ )}`
+ : this.renderPlaceholder();
+ const urlHtml = variant.url
+ ? this.buildLinkMarkup(variant.url)
+ : this.renderPlaceholder("No URL");
+ const hashHtml = variant.x
+ ? `${escape(
+ variant.x
+ )}`
+ : this.renderPlaceholder("Not provided");
+
+ const renderNestedList = (label, values, emptyLabel) => {
+ if (!Array.isArray(values) || values.length === 0) {
+ return `${escape( + label + )}
${this.renderListEmpty(emptyLabel)}${escape( + label + )}
${this.renderListEmpty(emptyLabel)}${escape( + label + )}
+${escape(
+ track.type
+ )}`
+ : this.renderPlaceholder();
+ const languageHtml = track.language
+ ? `${escape(
+ track.language
+ )}`
+ : this.renderPlaceholder();
+
+ return `
+ ${escape(segment.title)}
` + : this.renderPlaceholder("No title"); + const thumbnailHtml = segment.thumbnail + ? this.buildLinkMarkup(segment.thumbnail) + : this.renderPlaceholder("No thumbnail"); + + return ` +${escape(displayPubkey)}`
+ : this.renderPlaceholder("No pubkey");
+ const relayHtml = participant.relay
+ ? this.buildLinkMarkup(participant.relay, { breakAll: false })
+ : this.renderPlaceholder("No relay specified");
+
+ return `
+