diff --git a/js/app.js b/js/app.js index 2151fd28..6a1fa2d4 100644 --- a/js/app.js +++ b/js/app.js @@ -76,6 +76,7 @@ import { import { splitAndZap as splitAndZapDefault } from "./payments/zapSplit.js"; import { formatAbsoluteTimestamp as formatAbsoluteTimestampUtil, + formatAbsoluteDateWithOrdinal as formatAbsoluteDateWithOrdinalUtil, formatTimeAgo as formatTimeAgoUtil, truncateMiddle, formatShortNpub, @@ -8761,7 +8762,9 @@ class Application { (normalizedPostedAt === null || normalizedEditedAt - normalizedPostedAt >= 60); if (shouldShowEdited) { - payload.edited = `Last edited ${this.formatTimeAgo(normalizedEditedAt)}`; + const abs = formatAbsoluteDateWithOrdinalUtil(normalizedEditedAt); + const rel = this.formatTimeAgo(normalizedEditedAt); + payload.edited = `Last edited: ${abs} (${rel})`; } return payload; @@ -8846,6 +8849,21 @@ class Application { return null; } + // Prioritize NIP-71 published_at metadata if available + const rawNip71PublishedAt = + video?.nip71?.publishedAt || + video?.nip71?.published_at || + video?.nip71?.["published-at"]; + const parsedNip71PublishedAt = Number(rawNip71PublishedAt); + const nip71PublishedAt = Number.isFinite(parsedNip71PublishedAt) + ? Math.floor(parsedNip71PublishedAt) + : null; + + if (nip71PublishedAt !== null) { + this.cacheVideoRootCreatedAt(video, nip71PublishedAt); + return nip71PublishedAt; + } + const cached = this.getKnownVideoPostedAt(video); if (cached !== null) { return cached; diff --git a/js/ui/components/EditModal.js b/js/ui/components/EditModal.js index 92d1c70d..cb9ba308 100644 --- a/js/ui/components/EditModal.js +++ b/js/ui/components/EditModal.js @@ -438,9 +438,38 @@ export class EditModal { this.applyVideoToForm(editContext); this.nip71FormManager.resetSection(this.nip71SectionKey); - if (video.nip71 && typeof video.nip71 === "object") { - this.nip71FormManager.hydrateSection(this.nip71SectionKey, video.nip71); + + let nip71Data = video.nip71 && typeof video.nip71 === "object" ? { ...video.nip71 } : {}; + + // Backfill published_at if missing, using the original root creation time + if ( + !nip71Data.publishedAt && + !nip71Data.published_at && + !nip71Data["published-at"] + ) { + const rootCreated = video.rootCreatedAt || video.created_at; + if (rootCreated) { + nip71Data.publishedAt = rootCreated; + } } + + // Format publishedAt for datetime-local input (YYYY-MM-DDThh:mm) + const rawPublishedAt = + nip71Data.publishedAt || nip71Data.published_at || nip71Data["published-at"]; + if (rawPublishedAt) { + const numeric = Number(rawPublishedAt); + if (Number.isFinite(numeric)) { + const dt = new Date(numeric * 1000); + const pad = (n) => String(n).padStart(2, "0"); + const formatted = `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad( + dt.getDate() + )}T${pad(dt.getHours())}:${pad(dt.getMinutes())}`; + nip71Data.publishedAt = formatted; + } + } + + this.nip71FormManager.hydrateSection(this.nip71SectionKey, nip71Data); + const initialNip71 = this.nip71FormManager.collectSection(this.nip71SectionKey); this.originalNip71Metadata = this.cloneNip71Metadata(initialNip71); this.originalNip71MetadataJson = JSON.stringify(this.originalNip71Metadata); @@ -926,9 +955,23 @@ export class EditModal { const originalIsNsfw = original.isNsfw === true; const originalIsForKids = original.isForKids === true; - const currentNip71 = this.cloneNip71Metadata( - this.nip71FormManager.collectSection(this.nip71SectionKey) - ); + const rawNip71 = this.nip71FormManager.collectSection(this.nip71SectionKey); + const currentNip71 = this.cloneNip71Metadata(rawNip71); + + // Convert publishedAt back to Unix timestamp (seconds) + if (currentNip71 && typeof currentNip71.publishedAt === "string") { + const dateStr = currentNip71.publishedAt; + if (dateStr) { + const dt = new Date(dateStr); + if (!isNaN(dt.getTime())) { + currentNip71.publishedAt = Math.floor(dt.getTime() / 1000); + } else { + delete currentNip71.publishedAt; + } + } else { + delete currentNip71.publishedAt; + } + } const hasImetaSource = Array.isArray(currentNip71?.imeta) ? currentNip71.imeta.some((variant) => { if (!variant || typeof variant !== "object") { diff --git a/js/ui/components/VideoCard.js b/js/ui/components/VideoCard.js index efd19df1..d18b9202 100644 --- a/js/ui/components/VideoCard.js +++ b/js/ui/components/VideoCard.js @@ -951,6 +951,24 @@ export class VideoCard { this.viewCountEl = view; } + if ( + this.postedAt !== null && + Number.isFinite(this.video?.created_at) && + this.video.created_at > this.postedAt + 60 + ) { + const separator = this.createElement("span", { + classNames: ["text-muted-strong", isCompact ? "" : "mx-1"], + textContent: "•", + }); + separator.setAttribute("aria-hidden", "true"); + metadata.appendChild(separator); + + const edited = this.createElement("span", { + textContent: "Edited", + }); + metadata.appendChild(edited); + } + authorMeta.appendChild(authorName); authorMeta.appendChild(metadata); diff --git a/js/utils/formatters.js b/js/utils/formatters.js index a2af7f95..278bee49 100644 --- a/js/utils/formatters.js +++ b/js/utils/formatters.js @@ -90,3 +90,37 @@ export function formatTimeAgo(timestamp) { } return "just now"; } + +/** + * Formats a timestamp (in seconds) to a long absolute date with ordinal day + * (e.g. "January 6th, 2026"). + */ +export function formatAbsoluteDateWithOrdinal(timestamp) { + if (!Number.isFinite(timestamp)) { + return ""; + } + + const date = new Date(timestamp * 1000); + if (Number.isNaN(date.getTime())) { + return ""; + } + + // Helper for English ordinal suffixes + const getOrdinal = (n) => { + const s = ["th", "st", "nd", "rd"]; + const v = n % 100; + return s[(v - 20) % 10] || s[v] || s[0]; + }; + + try { + const month = date.toLocaleString("en-US", { month: "long" }); + const day = date.getDate(); + const year = date.getFullYear(); + const suffix = getOrdinal(day); + + return `${month} ${day}${suffix}, ${year}`; + } catch (err) { + // Fallback if locale fails + return date.toDateString(); + } +}