diff --git a/components/video-modal.html b/components/video-modal.html index 0a77155b..89a359c1 100644 --- a/components/video-modal.html +++ b/components/video-modal.html @@ -175,7 +175,13 @@
Posted just now - – views + + + – +
diff --git a/js/ui/components/SimilarContentCard.js b/js/ui/components/SimilarContentCard.js index 66c615a9..a759724d 100644 --- a/js/ui/components/SimilarContentCard.js +++ b/js/ui/components/SimilarContentCard.js @@ -2,6 +2,7 @@ import { normalizeDesignSystemContext } from "../../designSystem.js"; import { formatShortNpub } from "../../utils/formatters.js"; import { sanitizeProfileMediaUrl } from "../../utils/profileMedia.js"; +const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg"; export class SimilarContentCard { @@ -66,6 +67,7 @@ export class SimilarContentCard { this.authorNpubEl = null; this.timeEl = null; this.viewCountEl = null; + this.discussionCountEl = null; this.build(); } @@ -82,6 +84,10 @@ export class SimilarContentCard { return this.viewCountEl; } + getDiscussionCountElement() { + return this.discussionCountEl; + } + closeMoreMenu() {} closeSettingsMenu() {} @@ -469,9 +475,9 @@ export class SimilarContentCard { titleLink.title = this.video.title || "Untitled"; titleLink.style.minWidth = "0"; - // Modified order: Title -> Author -> Meta const authorStack = this.buildAuthorStack(); const metaRow = this.buildMetaRow(); + const engagement = this.buildEngagementSection(); content.appendChild(titleLink); if (authorStack) { @@ -480,6 +486,9 @@ export class SimilarContentCard { if (metaRow) { content.appendChild(metaRow); } + if (engagement) { + content.appendChild(engagement); + } this.contentEl = content; this.titleEl = titleLink; @@ -556,24 +565,6 @@ export class SimilarContentCard { row.classList.add("player-modal__similar-card-meta"); row.style.minWidth = "0"; - // Reordered: Views -> Separator -> Time - - const viewEl = this.document.createElement("span"); - viewEl.classList.add("player-modal__similar-card-views", "view-count-text"); - viewEl.dataset.viewCount = ""; - viewEl.textContent = "– views"; - if (this.pointerInfo?.key) { - viewEl.dataset.viewPointer = this.pointerInfo.key; - } - row.appendChild(viewEl); - this.viewCountEl = viewEl; - - const separator = this.document.createElement("span"); - separator.classList.add("player-modal__similar-card-separator"); - separator.setAttribute("aria-hidden", "true"); - separator.textContent = "•"; - row.appendChild(separator); - const timeEl = this.document.createElement("time"); timeEl.classList.add("player-modal__similar-card-timestamp"); if (this.postedAt !== null) { @@ -592,6 +583,146 @@ export class SimilarContentCard { return row; } + createEyeIcon(classNames = []) { + const svg = this.document.createElementNS(SVG_NAMESPACE, "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("aria-hidden", "true"); + svg.setAttribute("focusable", "false"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + + classNames.forEach((className) => { + if (className) { + svg.classList.add(className); + } + }); + + const path = this.document.createElementNS(SVG_NAMESPACE, "path"); + path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"); + svg.appendChild(path); + + const circle = this.document.createElementNS(SVG_NAMESPACE, "circle"); + circle.setAttribute("cx", "12"); + circle.setAttribute("cy", "12"); + circle.setAttribute("r", "3"); + svg.appendChild(circle); + + return svg; + } + + createMessageIcon(classNames = []) { + const svg = this.document.createElementNS(SVG_NAMESPACE, "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("aria-hidden", "true"); + svg.setAttribute("focusable", "false"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + + classNames.forEach((className) => { + if (className) { + svg.classList.add(className); + } + }); + + const path = this.document.createElementNS(SVG_NAMESPACE, "path"); + path.setAttribute( + "d", + "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" + ); + svg.appendChild(path); + + return svg; + } + + buildEngagementSection() { + const hasPointer = this.pointerInfo && this.pointerInfo.key; + const hasDiscussion = + this.video.enableComments !== false && + (typeof this.video.discussionCount === "number" || + (typeof this.video.discussionCount === "string" && + this.video.discussionCount.trim())); + + if (!hasPointer && !hasDiscussion) { + return null; + } + + const container = this.document.createElement("div"); + container.classList.add( + "flex", + "items-center", + "gap-4", + "text-xs", + "text-muted-strong", + "mt-1.5", + "video-card__engagement" // Reuse this class for consistent styling if global, otherwise it's just a marker + ); + // Add specific class for scoping if needed, but Tailwind classes usually suffice. + + // Views + if (hasPointer) { + const wrapper = this.document.createElement("div"); + wrapper.classList.add("flex", "items-center", "gap-1.5"); + wrapper.setAttribute("title", "Views"); + + const icon = this.createEyeIcon(["w-3.5", "h-3.5"]); + wrapper.appendChild(icon); + + const view = this.document.createElement("span"); + view.classList.add("view-count-text"); + view.textContent = "–"; + view.dataset.viewCount = ""; + view.dataset.viewPointer = this.pointerInfo.key; + + wrapper.appendChild(view); + container.appendChild(wrapper); + + this.viewCountEl = view; + } + + // Discussion + if (hasDiscussion) { + let initialCount = 0; + if (typeof this.video.discussionCount === "number") { + initialCount = this.video.discussionCount; + } else if (typeof this.video.discussionCount === "string") { + const parsed = Number.parseInt(this.video.discussionCount.trim(), 10); + if (Number.isFinite(parsed)) { + initialCount = parsed; + } + } + + if (Number.isFinite(initialCount) && initialCount >= 0) { + const wrapper = this.document.createElement("div"); + wrapper.classList.add("flex", "items-center", "gap-1.5"); + wrapper.dataset.discussionCount = this.video.id; + wrapper.dataset.countState = "ready"; + wrapper.setAttribute("title", "Comments"); + + const icon = this.createMessageIcon(["w-3.5", "h-3.5"]); + wrapper.appendChild(icon); + + const displayValue = initialCount.toLocaleString(); + + const valueEl = this.document.createElement("span"); + valueEl.textContent = displayValue; + valueEl.dataset.discussionCountValue = ""; + + wrapper.appendChild(valueEl); + container.appendChild(wrapper); + + this.discussionCountEl = wrapper; + } + } + + return container; + } + applyPointerDatasets() { if (!this.root || !this.pointerInfo) { return; diff --git a/js/ui/components/VideoCard.js b/js/ui/components/VideoCard.js index d18b9202..b068745c 100644 --- a/js/ui/components/VideoCard.js +++ b/js/ui/components/VideoCard.js @@ -476,9 +476,9 @@ export class VideoCard { content.appendChild(badgesContainer); } - const discussion = this.buildDiscussionCount(); - if (discussion) { - content.appendChild(discussion); + const engagement = this.buildEngagementSection(); + if (engagement) { + content.appendChild(engagement); } if (this.showUnsupportedTorrentBadge) { @@ -926,30 +926,7 @@ export class VideoCard { metadata.appendChild(timeEl); this.timestampEl = timeEl; - if (this.pointerInfo && this.pointerInfo.key) { - const dotClassNames = ["text-muted-strong"]; - if (!isCompact) { - dotClassNames.push("mx-1"); - } - - const dot = this.createElement("span", { - classNames: dotClassNames, - textContent: "•" - }); - dot.setAttribute("aria-hidden", "true"); - - const view = this.createElement("span", { - classNames: ["view-count-text"], - textContent: "– views" - }); - view.dataset.viewCount = ""; - view.dataset.viewPointer = this.pointerInfo.key; - - metadata.appendChild(dot); - metadata.appendChild(view); - - this.viewCountEl = view; - } + // (View count moved to engagement section) if ( this.postedAt !== null && @@ -2330,62 +2307,152 @@ export class VideoCard { } } - buildDiscussionCount() { - if (this.variant === "compact") { + createEyeIcon(classNames = []) { + const svg = this.document.createElementNS(SVG_NAMESPACE, "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("aria-hidden", "true"); + svg.setAttribute("focusable", "false"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + + classNames.forEach((className) => { + if (className) { + svg.classList.add(className); + } + }); + + const path = this.document.createElementNS(SVG_NAMESPACE, "path"); + path.setAttribute("d", "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"); + svg.appendChild(path); + + const circle = this.document.createElementNS(SVG_NAMESPACE, "circle"); + circle.setAttribute("cx", "12"); + circle.setAttribute("cy", "12"); + circle.setAttribute("r", "3"); + svg.appendChild(circle); + + return svg; + } + + createMessageIcon(classNames = []) { + const svg = this.document.createElementNS(SVG_NAMESPACE, "svg"); + svg.setAttribute("viewBox", "0 0 24 24"); + svg.setAttribute("fill", "none"); + svg.setAttribute("aria-hidden", "true"); + svg.setAttribute("focusable", "false"); + svg.setAttribute("stroke", "currentColor"); + svg.setAttribute("stroke-width", "2"); + svg.setAttribute("stroke-linecap", "round"); + svg.setAttribute("stroke-linejoin", "round"); + + classNames.forEach((className) => { + if (className) { + svg.classList.add(className); + } + }); + + const path = this.document.createElementNS(SVG_NAMESPACE, "path"); + path.setAttribute( + "d", + "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" + ); + svg.appendChild(path); + + return svg; + } + + buildEngagementSection() { + // Check availability + const hasPointer = this.pointerInfo && this.pointerInfo.key; + const hasDiscussion = + this.variant !== "compact" && + this.video.enableComments !== false && + (typeof this.video.discussionCount === "number" || + (typeof this.video.discussionCount === "string" && + this.video.discussionCount.trim())); + + if (!hasPointer && !hasDiscussion) { return null; } - if (this.video.enableComments === false) { - return null; + const container = this.createElement("div", { + classNames: [ + "flex", + "items-center", + "gap-4", + "text-xs", + "text-muted-strong", + "mt-3", + "video-card__engagement", + ], + }); + + // Views + if (hasPointer) { + const wrapper = this.createElement("div", { + classNames: ["flex", "items-center", "gap-1.5"], + }); + wrapper.setAttribute("title", "Views"); + + const icon = this.createEyeIcon(["w-4", "h-4"]); + wrapper.appendChild(icon); + + const view = this.createElement("span", { + classNames: ["view-count-text"], + textContent: "–", + }); + view.dataset.viewCount = ""; + view.dataset.viewPointer = this.pointerInfo.key; + + wrapper.appendChild(view); + container.appendChild(wrapper); + + this.viewCountEl = view; } - let initialCount = null; - if (typeof this.video.discussionCount === "number") { - initialCount = this.video.discussionCount; - } else if (typeof this.video.discussionCount === "string") { - const trimmed = this.video.discussionCount.trim(); - if (trimmed) { - const parsed = Number.parseInt(trimmed, 10); + // Discussion + if (hasDiscussion) { + let initialCount = 0; + if (typeof this.video.discussionCount === "number") { + initialCount = this.video.discussionCount; + } else if (typeof this.video.discussionCount === "string") { + const parsed = Number.parseInt(this.video.discussionCount.trim(), 10); if (Number.isFinite(parsed)) { initialCount = parsed; } } + + if (Number.isFinite(initialCount) && initialCount >= 0) { + const wrapper = this.createElement("div", { + classNames: ["flex", "items-center", "gap-1.5"], + }); + wrapper.dataset.discussionCount = this.video.id; + wrapper.dataset.countState = "ready"; + wrapper.setAttribute("title", "Comments"); + + const icon = this.createMessageIcon(["w-4", "h-4"]); + wrapper.appendChild(icon); + + const safeCount = Math.floor(initialCount); + const displayValue = this.helpers.toLocaleString + ? this.helpers.toLocaleString(safeCount) + : safeCount.toLocaleString(); + + const valueEl = this.createElement("span", { + textContent: displayValue, + }); + valueEl.dataset.discussionCountValue = ""; + + wrapper.appendChild(valueEl); + container.appendChild(wrapper); + + this.discussionCountEl = wrapper; + } } - if ( - initialCount === null || - !Number.isFinite(initialCount) || - initialCount < 0 - ) { - return null; - } - - const safeCount = Math.floor(initialCount); - const displayValue = this.helpers.toLocaleString - ? this.helpers.toLocaleString(safeCount) - : safeCount.toLocaleString(); - - const container = this.createElement("div", { - classNames: ["flex", "items-center", "text-xs", "text-muted-strong"] - }); - container.dataset.discussionCount = this.video.id; - container.dataset.countState = "ready"; - - const valueEl = this.createElement("span", { - textContent: displayValue - }); - valueEl.dataset.discussionCountValue = ""; - - const labelEl = this.createElement("span", { - classNames: ["ml-1"], - textContent: "notes" - }); - - container.appendChild(valueEl); - container.appendChild(labelEl); - - this.discussionCountEl = container; - return container; } diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 89fd1d93..e1501194 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -3959,8 +3959,17 @@ export class VideoModal { } updateViewCountLabel(text) { - if (this.videoViewCountEl) { - this.videoViewCountEl.textContent = text || ""; + if (!this.videoViewCountEl) { + return; + } + const textEl = this.videoViewCountEl.querySelector( + "[data-view-count-text]" + ); + if (textEl) { + textEl.textContent = text || "–"; + } else { + // Fallback if span is missing + this.videoViewCountEl.textContent = text || "–"; } } @@ -5438,15 +5447,14 @@ export class VideoModal { return this.formatViewCountLabel(Number(total)); } if (status === "hydrating") { - return "Loading views…"; + return "Loading…"; } - return "– views"; + return "–"; } formatViewCountLabel(total) { const numeric = Number.isFinite(total) ? Math.max(0, Number(total)) : 0; - const label = numeric === 1 ? "view" : "views"; - return `${formatViewCount(numeric)} ${label}`; + return formatViewCount(numeric); } syncMoreMenuData({ currentVideo, canManageBlacklist }) {