Merge pull request #1539 from PR0M3TH3AN/redesign-watch-history-cards-and-fix-timestamps-12937716587826727784

Redesign watch history cards and fix timestamp bug
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-22 21:11:25 -05:00
commit edfa3fb574
2 files changed
+166 -144

No files matched your search

+165 -144
View File
@@ -232,7 +232,9 @@ function normalizeHistoryItems(rawItems) {
? candidate.watchedAt
: Number.isFinite(candidate?.timestamp)
? candidate.timestamp
: null;
: Number.isFinite(pointer?.watchedAt)
? pointer.watchedAt
: null;
const watchedAt =
watchedAtRaw !== null ? Math.max(0, Math.floor(watchedAtRaw)) : 0;
normalized.push({
@@ -1117,17 +1119,34 @@ function refreshRegisteredHistoryCards(video, context, pointerKeys = null) {
});
}
export function buildHistoryCard({ item, video, profile }) {
export function buildHistoryCard({ item, video, profile, variant }) {
const isCompact = variant === "compact" || variant === "modal";
const article = document.createElement("article");
article.className = "watch-history-card";
article.dataset.pointerKey = item.pointerKey;
article.dataset.historyCard = "true";
if (video?.isPrivate) {
article.classList.add("watch-history-card--private");
article.className =
"watch-history-card group relative flex flex-row items-start gap-4 p-3 sm:p-4 rounded-lg transition-colors";
if (!isCompact) {
article.classList.add("hover:bg-surface-panel-hover", "hover:shadow-sm");
} else {
article.classList.add("hover:bg-surface-highlight");
}
const primary = document.createElement("div");
primary.className = "watch-history-card__primary";
if (video?.isPrivate) {
article.classList.add(
"border",
"border-status-info",
"border-opacity-40",
"bg-surface-alt"
);
}
article.dataset.pointerKey = item.pointerKey;
article.dataset.historyCard = "true";
const pointerVideoId = getPointerVideoId(video, item.pointer);
if (pointerVideoId) {
article.dataset.videoId = pointerVideoId;
}
const playbackData = (() => {
if (!video) {
@@ -1143,10 +1162,10 @@ export function buildHistoryCard({ item, video, profile }) {
return { url, magnet: magnetRaw };
})();
const pointerVideoId = getPointerVideoId(video, item.pointer);
// --- Thumbnail Column ---
const thumbnailLink = document.createElement("a");
thumbnailLink.className = "watch-history-card__thumbnail";
const thumbnailSizeClass = isCompact ? "w-28 sm:w-32" : "w-32 sm:w-48";
thumbnailLink.className = `relative block flex-shrink-0 aspect-video rounded-md overflow-hidden bg-surface-alt hover:opacity-90 transition-opacity ${thumbnailSizeClass}`;
thumbnailLink.href = "#";
thumbnailLink.dataset.historyAction = "play";
thumbnailLink.dataset.pointerKey = item.pointerKey;
@@ -1160,9 +1179,8 @@ export function buildHistoryCard({ item, video, profile }) {
thumbnailLink.dataset.playMagnet = playbackData.magnet;
}
const thumbnailInner = document.createElement("div");
thumbnailInner.className = "watch-history-card__thumbnailInner";
const thumbnailImg = document.createElement("img");
thumbnailImg.className = "w-full h-full object-cover";
thumbnailImg.alt = video?.title || "Video thumbnail";
const thumbnailSrc =
video && typeof video.thumbnail === "string" && video.thumbnail.trim()
@@ -1171,14 +1189,19 @@ export function buildHistoryCard({ item, video, profile }) {
thumbnailImg.src = thumbnailSrc;
thumbnailImg.loading = "lazy";
thumbnailImg.decoding = "async";
thumbnailInner.appendChild(thumbnailImg);
thumbnailLink.appendChild(thumbnailInner);
thumbnailLink.appendChild(thumbnailImg);
const details = document.createElement("div");
details.className = "watch-history-card__details";
// --- Content Column ---
const content = document.createElement("div");
content.className = "flex flex-col flex-1 min-w-0 gap-1.5";
// Title Row
const header = document.createElement("div");
header.className = "flex justify-between items-start gap-2";
const titleLink = document.createElement("a");
titleLink.className = "watch-history-card__title";
titleLink.className =
"text-sm font-semibold text-text line-clamp-2 leading-snug hover:text-accent transition-colors";
titleLink.href = "#";
titleLink.dataset.historyAction = "play";
titleLink.dataset.pointerKey = item.pointerKey;
@@ -1193,164 +1216,160 @@ export function buildHistoryCard({ item, video, profile }) {
}
titleLink.textContent = video?.title || "Untitled video";
const created = document.createElement("p");
created.className = "watch-history-card__created";
const createdAt = Number.isFinite(video?.created_at)
? video.created_at
: null;
created.textContent = createdAt
? `Published ${formatRelativeTime(createdAt)}`
: "Published date unavailable";
header.appendChild(titleLink);
content.appendChild(header);
details.appendChild(titleLink);
details.appendChild(created);
const moderationMount = document.createElement("div");
moderationMount.className = "watch-history-card__moderation flex flex-wrap items-center gap-sm";
details.appendChild(moderationMount);
// Author Row
const authorRow = document.createElement("div");
authorRow.className = "flex items-center gap-2";
primary.appendChild(thumbnailLink);
primary.appendChild(details);
const meta = document.createElement("div");
meta.className = "watch-history-card__meta";
const watched = document.createElement("p");
watched.className = "watch-history-card__watched";
watched.textContent = item.watchedAt
? `Watched ${formatRelativeTime(item.watchedAt)}`
: "Watched time unavailable";
meta.appendChild(watched);
const creator = document.createElement("div");
creator.className = "watch-history-card__creator";
const creatorAvatarButton = document.createElement("button");
creatorAvatarButton.type = "button";
creatorAvatarButton.className = "watch-history-card__creatorAvatar";
creatorAvatarButton.dataset.historyAction = "channel";
creatorAvatarButton.dataset.pointerKey = item.pointerKey;
const avatarBtn = document.createElement("button");
avatarBtn.type = "button";
avatarBtn.className =
"flex-shrink-0 w-5 h-5 rounded-full overflow-hidden bg-surface-alt hover:opacity-80 transition-opacity";
avatarBtn.dataset.historyAction = "channel";
avatarBtn.dataset.pointerKey = item.pointerKey;
if (video?.pubkey) {
creatorAvatarButton.dataset.author = video.pubkey;
avatarBtn.dataset.author = video.pubkey;
}
const avatarImg = document.createElement("img");
avatarImg.className = "w-full h-full object-cover";
const avatarSrc =
profile && typeof profile.picture === "string" && profile.picture.trim()
? profile.picture.trim()
: FALLBACK_AVATAR;
avatarImg.src = avatarSrc;
avatarImg.alt = profile?.name || profile?.display_name || "Creator avatar";
creatorAvatarButton.appendChild(avatarImg);
avatarImg.alt = profile?.name || "Avatar";
avatarBtn.appendChild(avatarImg);
const creatorNameButton = document.createElement("button");
creatorNameButton.type = "button";
creatorNameButton.className = "watch-history-card__creatorName";
creatorNameButton.dataset.historyAction = "channel";
creatorNameButton.dataset.pointerKey = item.pointerKey;
const authorNameBtn = document.createElement("button");
authorNameBtn.type = "button";
authorNameBtn.className =
"text-xs font-medium text-muted hover:text-text transition-colors truncate";
authorNameBtn.dataset.historyAction = "channel";
authorNameBtn.dataset.pointerKey = item.pointerKey;
if (video?.pubkey) {
creatorNameButton.dataset.author = video.pubkey;
authorNameBtn.dataset.author = video.pubkey;
}
const app = getAppInstance();
let creatorLabel =
profile?.display_name || profile?.name || profile?.username || "Unknown";
if ((!creatorLabel || creatorLabel === "Unknown") && video?.pubkey) {
const encoded = app?.safeEncodeNpub?.(video.pubkey) || "";
creatorLabel = formatShortNpub(encoded) || encoded || video.pubkey.slice(0, 8).concat("…");
creatorLabel =
formatShortNpub(encoded) || encoded || video.pubkey.slice(0, 8).concat("…");
}
creatorNameButton.textContent = creatorLabel;
authorNameBtn.textContent = creatorLabel;
creator.appendChild(creatorAvatarButton);
creator.appendChild(creatorNameButton);
meta.appendChild(creator);
authorRow.appendChild(avatarBtn);
authorRow.appendChild(authorNameBtn);
content.appendChild(authorRow);
const actions = document.createElement("div");
actions.className = "watch-history-card__actions";
// Moderation Badge Mount
const moderationMount = document.createElement("div");
moderationMount.className = "flex flex-wrap items-center gap-2 mt-0.5";
content.appendChild(moderationMount);
const playButton = document.createElement("button");
playButton.type = "button";
playButton.className =
"watch-history-card__action watch-history-card__action--primary";
playButton.dataset.historyAction = "play";
playButton.dataset.pointerKey = item.pointerKey;
if (pointerVideoId) {
playButton.dataset.videoId = pointerVideoId;
}
if (playbackData.url) {
playButton.dataset.playUrl = encodeURIComponent(playbackData.url);
}
if (playbackData.magnet) {
playButton.dataset.playMagnet = playbackData.magnet;
}
playButton.textContent = "Play";
// Meta Row (Timestamp)
const metaRow = document.createElement("div");
metaRow.className =
"flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-strong mt-auto pt-1";
const channelButton = document.createElement("button");
channelButton.type = "button";
channelButton.className = "watch-history-card__action";
channelButton.dataset.historyAction = "channel";
channelButton.dataset.pointerKey = item.pointerKey;
if (video?.pubkey) {
channelButton.dataset.author = video.pubkey;
if (item.watchedAt) {
const watchedSpan = document.createElement("span");
watchedSpan.textContent = formatRelativeTime(item.watchedAt);
watchedSpan.title = new Date(item.watchedAt * 1000).toLocaleString();
metaRow.appendChild(watchedSpan);
} else {
const unknownSpan = document.createElement("span");
unknownSpan.textContent = "Time unavailable";
metaRow.appendChild(unknownSpan);
}
channelButton.textContent = "Open channel";
const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.className =
"watch-history-card__action watch-history-card__action--danger";
removeButton.dataset.variant = "danger";
removeButton.dataset.historyAction = "remove";
removeButton.dataset.pointerKey = item.pointerKey;
removeButton.textContent = "Remove from history";
if (item.pointer?.type) {
removeButton.dataset.pointerType = item.pointer.type;
}
if (item.pointer?.value) {
removeButton.dataset.pointerValue = item.pointer.value;
}
if (item.pointer?.relay) {
removeButton.dataset.pointerRelay = item.pointer.relay;
}
if (Number.isFinite(item.watchedAt)) {
removeButton.dataset.pointerWatchedAt = String(item.watchedAt);
}
if (item.pointer?.session === true) {
removeButton.dataset.pointerSession = "true";
}
removeButton.setAttribute(
"aria-label",
"Remove this history entry (sync may take a moment)."
);
removeButton.title =
"Removes this entry from history. Relay sync may take a moment.";
if (video?.created_at) {
const sep = document.createElement("span");
sep.className = "text-muted/40";
sep.textContent = "•";
metaRow.appendChild(sep);
actions.appendChild(playButton);
actions.appendChild(channelButton);
actions.appendChild(removeButton);
meta.appendChild(actions);
article.appendChild(primary);
article.appendChild(meta);
if (pointerVideoId) {
article.dataset.videoId = pointerVideoId;
const pubSpan = document.createElement("span");
pubSpan.textContent = `Published ${formatRelativeTime(video.created_at)}`;
metaRow.appendChild(pubSpan);
}
content.appendChild(metaRow);
// Actions Row
const actionsRow = document.createElement("div");
actionsRow.className =
"flex flex-wrap items-center gap-4 mt-2 pt-2 border-t border-border/40";
const createActionBtn = (label, action, btnVariant = "default") => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = `text-xs font-semibold ${
btnVariant === "danger"
? "text-status-danger hover:text-status-danger-strong"
: "text-muted hover:text-text"
} transition-colors`;
btn.textContent = label;
btn.dataset.historyAction = action;
btn.dataset.pointerKey = item.pointerKey;
if (action === "play") {
if (pointerVideoId) btn.dataset.videoId = pointerVideoId;
if (playbackData.url)
btn.dataset.playUrl = encodeURIComponent(playbackData.url);
if (playbackData.magnet)
btn.dataset.playMagnet = playbackData.magnet;
} else if (action === "channel" && video?.pubkey) {
btn.dataset.author = video.pubkey;
} else if (action === "remove") {
if (item.pointer?.type) btn.dataset.pointerType = item.pointer.type;
if (item.pointer?.value) btn.dataset.pointerValue = item.pointer.value;
if (item.pointer?.relay) btn.dataset.pointerRelay = item.pointer.relay;
if (Number.isFinite(item.watchedAt))
btn.dataset.pointerWatchedAt = String(item.watchedAt);
if (item.pointer?.session === true)
btn.dataset.pointerSession = "true";
btn.title = "Remove from history";
}
return btn;
};
const playBtn = createActionBtn("Play now", "play", "default");
playBtn.classList.add("text-accent", "hover:text-accent-strong");
const channelBtn = createActionBtn("Channel", "channel");
const removeBtn = createActionBtn("Remove", "remove", "danger");
actionsRow.appendChild(playBtn);
actionsRow.appendChild(channelBtn);
actionsRow.appendChild(removeBtn);
content.appendChild(actionsRow);
article.appendChild(thumbnailLink);
article.appendChild(content);
const moderationContext = normalizeVideoModerationContext(video?.moderation);
const cardRef = {
pointerKey: item.pointerKey,
article,
primary,
meta,
details,
primary: thumbnailLink,
meta: content,
details: content,
badgeMount: moderationMount,
thumbnailInner,
thumbnailInner: null,
thumbnailLink,
titleLink,
playButton,
channelButton,
removeButton,
creatorNameButton,
avatarButton: creatorAvatarButton,
playButton: playBtn,
channelButton: channelBtn,
removeButton: removeBtn,
creatorNameButton: authorNameBtn,
avatarButton: avatarBtn,
video: video || null,
hiddenContainer: null,
badgeEl: null,
@@ -1420,7 +1439,8 @@ export function createWatchHistoryRenderer(config = {}) {
return controller.handleWatchHistoryRemoval(payload);
}
return defaultRemoveHandler(payload);
}
},
variant = "default"
} = config;
const syncEnabled =
@@ -1979,7 +1999,8 @@ export function createWatchHistoryRenderer(config = {}) {
const card = buildHistoryCard({
item: entry,
video: entry.metadata?.video || null,
profile: entry.metadata?.profile || null
profile: entry.metadata?.profile || null,
variant
});
container.list.appendChild(card);
});
+1
View File
@@ -1892,6 +1892,7 @@ export class ProfileModalController {
featureBannerSelector: "#profileHistoryFeatureBanner",
sessionWarningSelector: "#profileHistorySessionWarning",
emptyCopy: "You haven’t watched any videos yet.",
variant: "modal",
remove: (payload) =>
this.callbacks.onHistoryReady({
...(typeof payload === "object" && payload ? payload : {}),