Files
bitvid/js/trendingView.js
T
thePR0M3TH3ANandClaude Opus 4.8 02854d4fb5 feed: add Trending tab — recently-added ranked by view count (#27)
New FEED_TYPES.TRENDING tab: the active (recently-added) source ranked by view
count (recency tiebreak, trusted-muted last), gated by FEATURE_TRENDING_FEED
(default on). Builds on the now-trustworthy view counts (#4).

- createTrendingSorter reads counts from runtime.getViewCount, which resolves
  each video's pointer via the app's canonical deriveVideoPointerInfo against the
  shared viewCounter cache (new getVideoViewCountSnapshot export) — the same
  counts the grid cards already load.
- Live re-rank: viewCounter now emits a coalesced onViewCountsChanged signal;
  while the Trending view is active it debounce-re-runs the feed (cheap re-rank
  over the in-memory active set, no relay re-fetch), so the order settles into
  true trending as counts stream in. Cold cache reads as recency, then improves.
- Pipeline extracted to js/feedEngine/trendingFeed.js (feedCoordinator is at its
  size cap; bumped the cap for the thin load/refresh delegations).
- Wiring: FEED_TYPES + flag, feedCoordinator cases, app.js wrappers,
  applicationBootstrap registration (hides the sidebar link when flag off),
  viewManager init + js/trendingView.js, views/trending.html, sidebar link.

Tests: tests/trending-sorter.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-24 13:06:34 -04:00

66 lines
2.0 KiB
JavaScript

// js/trendingView.js
//
// The "Trending" tab presentation hook. The ranking lives in the feed engine
// (createTrendingSorter), but view counts arrive asynchronously as the grid
// cards subscribe to them. So while this view is active we listen for
// viewCounter's coalesced "counts changed" signal and re-run the Trending feed
// (a cheap re-rank over the in-memory active set, no relay re-fetch) so the order
// visibly settles into true trending as counts stream in.
import { attachFeedInfoPopover } from "./ui/components/FeedInfoPopover.js";
import { onViewCountsChanged } from "./viewCounter.js";
const TRENDING_RERANK_DEBOUNCE_MS = 700;
let unsubscribeCounts = null;
let rerankTimer = null;
export function teardownTrendingView() {
if (typeof unsubscribeCounts === "function") {
try {
unsubscribeCounts();
} catch (error) {
// best effort
}
unsubscribeCounts = null;
}
if (rerankTimer) {
clearTimeout(rerankTimer);
rerankTimer = null;
}
}
export function initTrendingView({ getApp, isActive } = {}) {
const infoTrigger = document.getElementById("trendingInfoTrigger");
if (infoTrigger) {
attachFeedInfoPopover(
infoTrigger,
"Trending ranks recently-added videos by their view count. The order settles as counts load in."
);
}
// Reset any prior subscription (re-entering the tab).
teardownTrendingView();
const stillActive = typeof isActive === "function" ? isActive : () => true;
unsubscribeCounts = onViewCountsChanged(() => {
if (rerankTimer) {
return;
}
rerankTimer = setTimeout(() => {
rerankTimer = null;
if (!stillActive()) {
teardownTrendingView();
return;
}
const app = typeof getApp === "function" ? getApp() : null;
if (app && typeof app.refreshTrendingFeed === "function") {
Promise.resolve(
app.refreshTrendingFeed({ reason: "view-count-update" })
).catch(() => {});
}
}, TRENDING_RERANK_DEBOUNCE_MS);
});
}