Files
bitvid/js/zapEventsFacade.js
thePR0M3TH3ANandClaude Opus 4.8 974fdc836d charts: dual-line popularity chart — views (red) + zaps (orange) over time (#47 §5.9)
The per-video Popularity modal now plots a second, orange zaps-over-time line
beside the red views line, with a legend and — new — VISIBLE x-axis date ticks
(the old chart only put the range in the aria-label).

- buildZapSatsTimeSeries: buckets cumulative SATS from 9735 receipts + verified
  bitvid tallies, deduped by payment_hash (same rule as the badge store) so the
  chart and badge agree.
- buildPopularityChartSvg: two token-colored cumulative lines on a shared time
  x-axis, each with its OWN y scale (independent per-series — D5), plus date-tick
  <text> labels. Views = .text-accent, zaps = .text-zap (new --color-zap
  utility). Legend is plain DOM in the modal; headline gains "· N sats zapped".
- js/zapEventsFacade.js: lists a pointer's [9735, ZAP_TALLY_KIND] events via the
  L1 SubscriptionManager for the chart; modal re-renders on onZapTotalsChanged.
- All flag-gated (FEATURE_ZAP_TALLY); the existing views-only chart + its test
  are untouched (buildViewCountChartSvg kept).

Verified live earlier that imports resolve (app builds/runs). Tests:
tests/zap-time-series-chart.test.mjs (5) + existing view-count-chart green.
Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-05 00:27:00 -04:00

72 lines
2.2 KiB
JavaScript

// js/zapEventsFacade.js
//
// Lists the zap events for a video/profile pointer so the Popularity chart can
// draw a zaps-over-time series (docs/zap-tally-plan.md §5.9). Fetches BOTH real
// NIP-57 receipts (kind 9735) and bitvid tallies (ZAP_TALLY_KIND); the chart's
// buildZapSatsTimeSeries verifies tallies + dedups by payment_hash, so this just
// needs to return the raw events. Routed through the L1 SubscriptionManager
// (relay caps + in-flight dedupe), matching the zap-totals store.
import { nostrClient } from "./nostrClientFacade.js";
import { ZAP_RECEIPT_KIND } from "./zapTotals.js";
import { ZAP_TALLY_KIND } from "./nostrEventSchemas.js";
import { devLogger } from "./utils/logger.js";
const LIMIT = 500;
// pointer: the tag-style ["a"|"e", value, relay?] array resolveVideoPointer
// produces (or an { type, value } object).
function pointerFilterKey(pointer) {
const type = Array.isArray(pointer)
? pointer[0]
: pointer && typeof pointer === "object"
? pointer.type
: "";
const value = Array.isArray(pointer)
? pointer[1]
: pointer && typeof pointer === "object"
? pointer.value
: "";
if ((type === "a" || type === "e") && typeof value === "string" && value.trim()) {
return { tag: `#${type}`, value: value.trim() };
}
return null;
}
export async function listVideoZapEventsWithDefaultClient(
pointer,
{ since, client = nostrClient } = {},
) {
const pf = pointerFilterKey(pointer);
if (!pf) {
return [];
}
const manager =
typeof client?.getSubscriptionManager === "function"
? client.getSubscriptionManager()
: null;
const relays = Array.isArray(client?.relays) ? client.relays : [];
if (!manager || typeof manager.list !== "function" || !relays.length) {
return [];
}
const filter = {
kinds: [ZAP_RECEIPT_KIND, ZAP_TALLY_KIND],
[pf.tag]: [pf.value],
limit: LIMIT,
};
if (Number.isFinite(since) && since > 0) {
filter.since = Math.floor(since);
}
try {
const events = await manager.list({ relays, filters: [filter] });
return Array.isArray(events) ? events : [];
} catch (error) {
devLogger.warn("[zapEvents] Failed to list zap events:", error);
return [];
}
}
export default listVideoZapEventsWithDefaultClient;