mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
71 lines
2.3 KiB
JavaScript
71 lines
2.3 KiB
JavaScript
// #27: the Trending feed ranks the active set by VIEW COUNT (recency breaks
|
|
// ties), with unknown/not-yet-loaded counts treated as 0 and trusted-muted last.
|
|
// The sorter reads counts from the injected runtime.getViewCount (the shared
|
|
// viewCounter cache).
|
|
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createTrendingSorter } from "../js/feedEngine/sorters.js";
|
|
|
|
const item = (id, { views, created_at = 1000, muted = false } = {}) => ({
|
|
video: {
|
|
id,
|
|
pubkey: "a".repeat(64),
|
|
created_at,
|
|
...(muted ? { moderation: { trustedMuted: true } } : {}),
|
|
},
|
|
metadata: {},
|
|
__views: views,
|
|
});
|
|
|
|
const sort = (items) => {
|
|
const counts = new Map(items.map((i) => [i.video.id, i.__views]));
|
|
return createTrendingSorter()(items, {
|
|
runtime: { getViewCount: (video) => counts.get(video?.id) },
|
|
}).map((i) => i.video.id);
|
|
};
|
|
|
|
test("ranks by view count descending", () => {
|
|
assert.deepEqual(
|
|
sort([item("low", { views: 5 }), item("high", { views: 100 }), item("mid", { views: 50 })]),
|
|
["high", "mid", "low"],
|
|
);
|
|
});
|
|
|
|
test("recency breaks ties on equal views", () => {
|
|
assert.deepEqual(
|
|
sort([
|
|
item("older", { views: 10, created_at: 1000 }),
|
|
item("newer", { views: 10, created_at: 2000 }),
|
|
]),
|
|
["newer", "older"],
|
|
);
|
|
});
|
|
|
|
test("unknown counts are treated as 0 (rank below known views, recency among them)", () => {
|
|
const ids = sort([
|
|
item("unknown-old", { views: undefined, created_at: 1000 }),
|
|
item("known", { views: 3, created_at: 500 }),
|
|
item("unknown-new", { views: undefined, created_at: 2000 }),
|
|
]);
|
|
assert.equal(ids[0], "known", "a known-view video leads the not-yet-loaded ones");
|
|
assert.deepEqual(ids.slice(1), ["unknown-new", "unknown-old"], "unknowns fall back to recency");
|
|
});
|
|
|
|
test("trusted-muted sinks last even if most-viewed", () => {
|
|
assert.deepEqual(
|
|
sort([item("muted-popular", { views: 1000, muted: true }), item("plain", { views: 1 })]),
|
|
["plain", "muted-popular"],
|
|
);
|
|
});
|
|
|
|
test("tolerates non-array input and a missing getViewCount", () => {
|
|
assert.deepEqual(createTrendingSorter()(null, {}), []);
|
|
// No runtime.getViewCount → all 0 → recency order.
|
|
const out = createTrendingSorter()(
|
|
[item("a", { created_at: 1 }), item("b", { created_at: 2 })],
|
|
{},
|
|
).map((i) => i.video.id);
|
|
assert.deepEqual(out, ["b", "a"]);
|
|
});
|