mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +00:00
[channel] Show view counts on channel-profile cards (were blank)
Channel-profile video cards rendered blank view counts. The main feed subscribes each card's [data-view-count] element to the shared view counter (VideoListView.ensureViewCountSubscription / registerVideoViewCountElement), but the channel-profile grid renders its own VideoCards and never did — so nothing ever populated the element. Add js/channelViewCounts.js (subscribeChannelCardViewCount / clearChannelCardViewCounts) and call it from channelProfile.js after each card's getRoot(); teardown runs in clearChannelVideoCardRegistry (already invoked on grid re-render / channel close). Logic lives in a new module so channelProfile.js stays under its size cap. On "are they counted correctly?": yes — this subscribes to the SAME batched kind-30079 view-count transport the feed uses (one shared subscription + the deduped cache), so the channel now shows the exact same counts the feed does. The counting itself was never the issue; the channel grid just wasn't displaying it. Tests: tests/channel-view-counts.test.mjs (3 — non-blank label, safe no-ops, idempotent teardown). Lint + build green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
6b251dbda5
commit
9e5cb9f57b
3 files changed
+150
No files matched your search
@@ -13,6 +13,10 @@ import { escapeHTML } from "./utils/domUtils.js";
|
||||
import { formatShortNpub } from "./utils/formatters.js";
|
||||
import createPopover from "./ui/overlay/popoverEngine.js";
|
||||
import { VideoCard } from "./ui/components/VideoCard.js";
|
||||
import {
|
||||
subscribeChannelCardViewCount,
|
||||
clearChannelCardViewCounts,
|
||||
} from "./channelViewCounts.js";
|
||||
import { createChannelProfileMenuPanel } from "./ui/components/videoMenuRenderers.js";
|
||||
import { ALLOW_NSFW_CONTENT } from "./config.js";
|
||||
import { sanitizeProfileMediaUrl } from "./utils/profileMedia.js";
|
||||
@@ -179,6 +183,7 @@ function updateChannelBannerBackground(el, url) {
|
||||
|
||||
export function clearChannelVideoCardRegistry() {
|
||||
channelVideoCardsById.clear();
|
||||
clearChannelCardViewCounts();
|
||||
}
|
||||
|
||||
let currentChannelHex = null;
|
||||
@@ -5029,6 +5034,9 @@ export async function renderChannelVideosFromList({
|
||||
const cardEl = videoCard.getRoot();
|
||||
if (cardEl) {
|
||||
fragment.appendChild(cardEl);
|
||||
// Subscribe the card to the shared view counter (the feed does this; the
|
||||
// channel grid didn't, so its view counts rendered blank).
|
||||
subscribeChannelCardViewCount(cardEl, pointerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// Wire channel-profile video cards to the shared view counter. The main feed does
|
||||
// this in VideoListView (ensureViewCountSubscription / registerVideoViewCountElement);
|
||||
// the channel-profile grid renders its own VideoCards and never subscribed them, so
|
||||
// every card's view count rendered blank. This mirrors the feed's behavior against
|
||||
// the SAME batched view-count transport (one shared kind-30079 subscription), so the
|
||||
// counts are identical to what the feed shows — the channel just wasn't displaying
|
||||
// them (the counting itself is unchanged and shared).
|
||||
|
||||
import {
|
||||
subscribeToVideoViewCount,
|
||||
unsubscribeFromVideoViewCount,
|
||||
formatViewCount,
|
||||
} from "./viewCounter.js";
|
||||
import { devLogger } from "./utils/logger.js";
|
||||
|
||||
const activeSubscriptions = [];
|
||||
|
||||
function formatViewCountLabel(total) {
|
||||
const value = Number.isFinite(total) ? Number(total) : 0;
|
||||
return `${formatViewCount(value)} ${value === 1 ? "view" : "views"}`;
|
||||
}
|
||||
|
||||
// Subscribe one channel card's [data-view-count] element to its pointer.
|
||||
export function subscribeChannelCardViewCount(cardEl, pointerInfo) {
|
||||
const pointer = pointerInfo?.pointer;
|
||||
if (!cardEl || !pointer) {
|
||||
return;
|
||||
}
|
||||
const el = cardEl.querySelector("[data-view-count]");
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
if (!el.textContent || !el.textContent.trim()) {
|
||||
el.textContent = "– views";
|
||||
}
|
||||
|
||||
try {
|
||||
const token = subscribeToVideoViewCount(
|
||||
pointer,
|
||||
({ total, status, partial }) => {
|
||||
let text;
|
||||
if (Number.isFinite(total)) {
|
||||
text = formatViewCountLabel(total);
|
||||
if (partial) {
|
||||
text = `${text} (partial)`;
|
||||
}
|
||||
} else if (status === "hydrating") {
|
||||
text = "Loading views…";
|
||||
} else {
|
||||
text = "– views";
|
||||
}
|
||||
if (el.isConnected) {
|
||||
el.textContent = text;
|
||||
el.dataset.viewCountState = partial ? "partial" : status;
|
||||
}
|
||||
},
|
||||
);
|
||||
activeSubscriptions.push({ pointer, token });
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[ChannelProfile] Failed to subscribe card view count:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Tear down every channel-card view-count subscription (on grid re-render / close).
|
||||
export function clearChannelCardViewCounts() {
|
||||
while (activeSubscriptions.length) {
|
||||
const { pointer, token } = activeSubscriptions.pop();
|
||||
try {
|
||||
unsubscribeFromVideoViewCount(pointer, token);
|
||||
} catch (error) {
|
||||
// best-effort teardown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Channel-profile cards showed BLANK view counts because the channel grid never
|
||||
// subscribed its cards to the shared view counter (the main feed does). This wires
|
||||
// each card's [data-view-count] element the same way. The count VALUES come from the
|
||||
// same shared transport as the feed — the channel just wasn't displaying them.
|
||||
//
|
||||
// test_integrity_note:
|
||||
// change_type: ["new_tests"]
|
||||
// scenarios:
|
||||
// - id: SCN-channel-card-view-count
|
||||
// given: "a channel card element with a [data-view-count] span + a pointer"
|
||||
// when: "subscribeChannelCardViewCount runs (and clearChannelCardViewCounts tears down)"
|
||||
// then: "the element gets a non-blank view label; missing pointer/element/card are safe no-ops"
|
||||
// observable_outcomes:
|
||||
// - "a card with a pointer gets a non-blank view label (no longer blank)"
|
||||
// - "no pointer / no [data-view-count] / no card -> no throw, no label forced"
|
||||
// - "clearChannelCardViewCounts is idempotent and never throws"
|
||||
// determinism_controls:
|
||||
// - "JSDOM element; real module against the (uninitialised) shared view counter"
|
||||
// anti_cheat_rationale:
|
||||
// prevents: ["asserting the blank (broken) state", "hard-coded return value"]
|
||||
// relaxation:
|
||||
// did_relax_any_assertion: false
|
||||
|
||||
import "./test-helpers/setup-localstorage.mjs";
|
||||
import test from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { JSDOM } from "jsdom";
|
||||
import {
|
||||
subscribeChannelCardViewCount,
|
||||
clearChannelCardViewCounts,
|
||||
} from "../js/channelViewCounts.js";
|
||||
|
||||
function makeCard(html) {
|
||||
const dom = new JSDOM(`<!DOCTYPE html><body>${html}</body>`);
|
||||
return dom.window.document.body.firstElementChild;
|
||||
}
|
||||
|
||||
test("a card with a pointer gets a non-blank view-count label (fixes the blank)", () => {
|
||||
const card = makeCard('<div class="card"><span data-view-count></span></div>');
|
||||
subscribeChannelCardViewCount(card, { pointer: ["a", 30078, "root-1"], key: "k1" });
|
||||
const el = card.querySelector("[data-view-count]");
|
||||
assert.ok(el.textContent && el.textContent.trim().length > 0, "label is not blank");
|
||||
assert.match(el.textContent, /view|Loading/i, "shows a view/loading label");
|
||||
clearChannelCardViewCounts();
|
||||
});
|
||||
|
||||
test("safe no-ops: missing pointer, missing [data-view-count], or missing card", () => {
|
||||
const cardNoEl = makeCard('<div class="card"></div>');
|
||||
const cardWithEl = makeCard('<div class="card"><span data-view-count></span></div>');
|
||||
assert.doesNotThrow(() => {
|
||||
subscribeChannelCardViewCount(cardWithEl, {}); // no pointer
|
||||
subscribeChannelCardViewCount(cardNoEl, { pointer: ["a", 30078, "x"] }); // no element
|
||||
subscribeChannelCardViewCount(null, { pointer: ["a", 30078, "x"] }); // no card
|
||||
});
|
||||
// A card with no pointer must not have its (absent) element forced to a label.
|
||||
assert.equal(cardWithEl.querySelector("[data-view-count]").textContent, "");
|
||||
clearChannelCardViewCounts();
|
||||
});
|
||||
|
||||
test("clearChannelCardViewCounts is idempotent and never throws", () => {
|
||||
assert.doesNotThrow(() => {
|
||||
clearChannelCardViewCounts();
|
||||
clearChannelCardViewCounts();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user