mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #835 from PR0M3TH3AN/codex/add-popular-tags-feature-to-video-list
Add popular tag pills to recent videos view
This commit is contained in:
5 files changed
+202
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -1123,6 +1123,9 @@ class SubscriptionsManager {
|
||||
};
|
||||
|
||||
const listView = new VideoListView(listViewConfig);
|
||||
if (typeof listView.setPopularTagsContainer === "function") {
|
||||
listView.setPopularTagsContainer(null);
|
||||
}
|
||||
|
||||
listView.setPlaybackHandler((detail) => {
|
||||
if (!detail) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user