Add popular tag strip support to recent videos

This commit is contained in:
thePR0M3TH3AN committed 2025-10-23 15:13:05 -04:00
1 parent 62e9ff54fc
commit 876020b90f
5 files changed
+202

No files matched your search

+21
View File
@@ -887,6 +887,7 @@ class Application {
this.currentVideoPointerKey = null;
this.videoSubscription = this.nostrService.getVideoSubscription() || null;
this.videoList = null;
this.videoListPopularTags = null;
this.modalViewCountUnsub = null;
this.modalReactionUnsub = null;
this.modalReactionPointerKey = null;
@@ -1197,6 +1198,11 @@ class Application {
}
this.videoList = null;
if (this.videoListPopularTags) {
this.videoListPopularTags.textContent = "";
this.videoListPopularTags.hidden = true;
}
this.videoListPopularTags = null;
if (this.mediaLoader && typeof this.mediaLoader.disconnect === "function") {
this.mediaLoader.disconnect();
@@ -2902,6 +2908,11 @@ class Application {
const target = container || document.getElementById("videoList");
this.videoList = target || null;
const tagsRoot = document.getElementById("recentVideoTags");
this.videoListPopularTags = tagsRoot || null;
if (typeof this.videoListView.setPopularTagsContainer === "function") {
this.videoListView.setPopularTagsContainer(this.videoListPopularTags);
}
this.videoListView.mount(this.videoList || null);
return this.videoList;
}
@@ -2933,6 +2944,11 @@ class Application {
: "Refreshing videos…";
this.videoList = isElement(container) ? container : null;
const tagsRoot = document.getElementById("recentVideoTags");
this.videoListPopularTags = isElement(tagsRoot) ? tagsRoot : null;
if (typeof this.videoListView.setPopularTagsContainer === "function") {
this.videoListView.setPopularTagsContainer(this.videoListPopularTags);
}
if (this.videoList) {
this.videoList.innerHTML = getSidebarLoadingMarkup(messageContext);
@@ -8831,6 +8847,11 @@ class Application {
}
this.videoList = null;
if (this.videoListPopularTags) {
this.videoListPopularTags.textContent = "";
this.videoListPopularTags.hidden = true;
}
this.videoListPopularTags = null;
}
shareActiveVideo() {
+3
View File
@@ -1123,6 +1123,9 @@ class SubscriptionsManager {
};
const listView = new VideoListView(listViewConfig);
if (typeof listView.setPopularTagsContainer === "function") {
listView.setPopularTagsContainer(null);
}
listView.setPlaybackHandler((detail) => {
if (!detail) {
+97
View File
@@ -1,10 +1,12 @@
import { VideoCard } from "../components/VideoCard.js";
import { renderTagPillStrip } from "../components/tagPillList.js";
import {
subscribeToVideoViewCount,
unsubscribeFromVideoViewCount,
} from "../../viewCounter.js";
import { normalizeDesignSystemContext } from "../../designSystem.js";
import { userLogger } from "../../utils/logger.js";
import { collectVideoTags } from "../../utils/videoTags.js";
const EMPTY_VIDEO_LIST_SIGNATURE = "__EMPTY__";
@@ -174,6 +176,8 @@ export class VideoListView {
this.currentVideos = [];
this.lastRenderedVideoSignature = null;
this._lastRenderedVideoListElement = null;
this.popularTagsRoot = null;
this._popularTagStrip = null;
this.handlers = {
playback: null,
@@ -256,6 +260,12 @@ export class VideoListView {
this.currentVideos = [];
this.lastRenderedVideoSignature = null;
this._lastRenderedVideoListElement = null;
if (this.popularTagsRoot) {
this.popularTagsRoot.textContent = "";
this.popularTagsRoot.hidden = true;
}
this.popularTagsRoot = null;
this._popularTagStrip = null;
}
setPlaybackHandler(handler) {
@@ -282,6 +292,91 @@ export class VideoListView {
this.handlers.moderationOverride = typeof handler === "function" ? handler : null;
}
setPopularTagsContainer(container) {
if (this.popularTagsRoot && this.popularTagsRoot !== container) {
this.popularTagsRoot.textContent = "";
this.popularTagsRoot.hidden = true;
}
this.popularTagsRoot = container || null;
this._popularTagStrip = null;
if (this.popularTagsRoot) {
this.popularTagsRoot.textContent = "";
this.popularTagsRoot.hidden = true;
}
}
updatePopularTags(videos) {
const root = this.popularTagsRoot;
if (!root) {
return;
}
if (!Array.isArray(videos) || videos.length === 0) {
root.textContent = "";
root.hidden = true;
this._popularTagStrip = null;
return;
}
const counts = new Map();
const displayNames = new Map();
videos.forEach((video) => {
const tags = collectVideoTags(video);
tags.forEach((tag) => {
if (typeof tag !== "string" || !tag) {
return;
}
const lower = tag.toLowerCase();
counts.set(lower, (counts.get(lower) || 0) + 1);
if (!displayNames.has(lower)) {
displayNames.set(lower, tag);
}
});
});
if (!counts.size) {
root.textContent = "";
root.hidden = true;
this._popularTagStrip = null;
return;
}
const tagEntries = Array.from(counts.entries()).map(([lower, count]) => ({
count,
tag: displayNames.get(lower) || lower,
}));
tagEntries.sort((a, b) => {
if (b.count !== a.count) {
return b.count - a.count;
}
const lowerA = a.tag.toLowerCase();
const lowerB = b.tag.toLowerCase();
if (lowerA === lowerB) {
return a.tag.localeCompare(b.tag);
}
return lowerA.localeCompare(lowerB);
});
const tags = tagEntries.map((entry) => entry.tag);
const doc = this.document || root.ownerDocument || null;
if (!doc) {
root.textContent = "";
root.hidden = true;
this._popularTagStrip = null;
return;
}
const { root: strip } = renderTagPillStrip({ document: doc, tags });
root.textContent = "";
root.appendChild(strip);
root.hidden = false;
this._popularTagStrip = strip;
}
render(videos, metadata = null) {
if (!this.container) {
return [];
@@ -307,6 +402,8 @@ export class VideoListView {
return canEdit || !isPrivate;
});
this.updatePopularTags(displayVideos);
this.syncViewCountSubscriptions(displayVideos);
this.cleanupThumbnailCache(displayVideos);
this.cleanupHealthCache(displayVideos);
@@ -0,0 +1,80 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import { VideoListView } from "../../js/ui/views/VideoListView.js";
function createViewDom() {
const dom = new JSDOM(
"<!DOCTYPE html><body><section><div id=\"recentVideoTags\" hidden></div><div id=\"videoList\"></div></section></body>"
);
const { document } = dom.window;
const listRoot = document.getElementById("videoList");
const tagsRoot = document.getElementById("recentVideoTags");
return { dom, document, listRoot, tagsRoot };
}
test("VideoListView renders sorted popular tag pills", () => {
const { document, listRoot, tagsRoot } = createViewDom();
const view = new VideoListView({ document });
view.mount(listRoot);
view.setPopularTagsContainer(tagsRoot);
const videos = [
{
id: "v1",
title: "First",
pubkey: "pub1",
created_at: 3,
nip71: { hashtags: ["Nostr", "Video"] },
},
{
id: "v2",
title: "Second",
pubkey: "pub2",
created_at: 2,
tags: [
["t", "nostr"],
["t", "Learning"],
],
},
{
id: "v3",
title: "Third",
pubkey: "pub3",
created_at: 1,
nip71: { hashtags: ["video"] },
},
];
view.render(videos);
assert(tagsRoot);
assert.equal(tagsRoot.hidden, false);
const buttons = tagsRoot.querySelectorAll("button");
assert.equal(buttons.length, 3);
assert.deepEqual(
Array.from(buttons, (button) => button.dataset.tag),
["Nostr", "Video", "Learning"],
);
});
test("VideoListView hides popular tags when no tags are available", () => {
const { document, listRoot, tagsRoot } = createViewDom();
const view = new VideoListView({ document });
view.mount(listRoot);
view.setPopularTagsContainer(tagsRoot);
view.render([
{
id: "v10",
title: "Tagless",
pubkey: "pub10",
created_at: 10,
},
]);
assert(tagsRoot);
assert.equal(tagsRoot.hidden, true);
assert.equal(tagsRoot.childElementCount, 0);
});
+1
View File
@@ -4,6 +4,7 @@
<h2 class="mb-4 text-xl font-semibold text-text">Most Recent Videos</h2>
<!-- Video List -->
<div id="recentVideoTags" class="mb-4" hidden></div>
<div id="videoList" class="bv-grid-video">
<div class="sidebar-loading-wrapper" role="status" aria-live="polite">
<div class="sidebar-loading-indicator">