From 1bc2b063958cd30b99487d323b83acc68ffbd9f6 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:07:20 -0400 Subject: [PATCH] fix(popularity): headline reads the authoritative view counter, not a re-count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chart showed a different (higher, fluctuating) total than the card's view count because it re-derived its own total from a one-off, unwindowed relay listVideoViewEvents — which varies by relay response and counts a wider set than the counter. Now the modal headline subscribes to the same viewCounter the card uses (the deduped, cached, authoritative total), so it always matches the number shown everywhere and is stable. The chart below is purely the time DISTRIBUTION of recorded views, listed within the counter's backfill window (VIEW_COUNT_BACKFILL_MAX_DAYS) so it lines up with the headline's window. The count subscription is cleaned up on close. Co-Authored-By: Claude Opus 4.8 --- js/viewCountChart.js | 62 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/js/viewCountChart.js b/js/viewCountChart.js index 295e5be9..c9c8cc2c 100644 --- a/js/viewCountChart.js +++ b/js/viewCountChart.js @@ -10,7 +10,12 @@ import { subscribeVideoViewEventsWithDefaultClient, } from "./nostrViewEventsFacade.js"; import { resolveVideoPointer } from "./utils/videoPointer.js"; -import { formatViewCount } from "./viewCounter.js"; +import { + formatViewCount, + subscribeToVideoViewCount, + unsubscribeFromVideoViewCount, +} from "./viewCounter.js"; +import { VIEW_COUNT_BACKFILL_MAX_DAYS } from "./config.js"; import { devLogger } from "./utils/logger.js"; export const VIEW_CHART_WINDOW_SECONDS = 86400; // 1 day, matches the counter dedupe window @@ -226,6 +231,8 @@ export function openPopularityModal({ const eventsById = new Map(); let closed = false; let unsubscribe = null; + let countToken = null; + let counterTotal = null; let rerenderTimer = null; const addEvent = (event) => { @@ -235,22 +242,37 @@ export function openPopularityModal({ return true; }; - const render = () => { + // The headline number must match what the card / counter shows everywhere + // (the authoritative, deduped, cached viewCounter total) — NOT a separate count + // re-derived from a one-off relay list (which varies by relay response and + // window). The chart below shows only the time DISTRIBUTION of recorded views. + const renderHeadline = () => { if (closed) return; - const { series, total } = buildViewCountTimeSeries([...eventsById.values()]); + const total = Number.isFinite(counterTotal) + ? counterTotal + : buildViewCountTimeSeries([...eventsById.values()]).total; + if (!Number.isFinite(total)) { + return; + } totalLine.textContent = total > 0 ? `${formatViewCount(total)} ${total === 1 ? "view" : "views"}` : "No views recorded yet."; + }; + + const renderChart = () => { + if (closed) return; + const { series } = buildViewCountTimeSeries([...eventsById.values()]); chartHost.textContent = ""; chartHost.appendChild(buildViewCountChartSvg(doc, series)); + renderHeadline(); }; const scheduleRender = () => { if (rerenderTimer) return; rerenderTimer = setTimeout(() => { rerenderTimer = null; - render(); + renderChart(); }, 250); }; @@ -268,6 +290,13 @@ export function openPopularityModal({ // best effort } } + if (countToken != null) { + try { + unsubscribeFromVideoViewCount(pointer, countToken); + } catch (error) { + // best effort + } + } doc.removeEventListener("keydown", onKeydown, true); backdrop.remove(); if (activePopularityModal === handle) { @@ -288,19 +317,32 @@ export function openPopularityModal({ }); doc.addEventListener("keydown", onKeydown, true); - // Initial fetch + live subscription. + // Headline total: subscribe to the same counter the card uses (stable + cached). + try { + countToken = subscribeToVideoViewCount(pointer, ({ total } = {}) => { + if (closed) return; + if (Number.isFinite(total)) { + counterTotal = Number(total); + renderHeadline(); + } + }); + } catch (error) { + devLogger.warn("[popularity] Failed to subscribe to view count:", error); + } + + // Chart shape: list the recorded view events within the counter's backfill + // window so the distribution lines up with the headline's window. + const since = + Math.floor(Date.now() / 1000) - VIEW_COUNT_BACKFILL_MAX_DAYS * 86400; Promise.resolve() - .then(() => listViewEvents(pointer, {})) + .then(() => listViewEvents(pointer, { since })) .then((events) => { if (closed) return; (Array.isArray(events) ? events : []).forEach(addEvent); - render(); + renderChart(); }) .catch((error) => { devLogger.warn("[popularity] Failed to load view events:", error); - if (!closed) { - totalLine.textContent = "Couldn’t load view data."; - } }); try {