Files
bitvid/js/feedEngine/mostZappedFeed.js
T
thePR0M3TH3ANandClaude Fable 5 8dfd2034b0 feeds: "Most Zapped" sidebar tab ranked by zap totals (#47)
Trending-pattern clone ranked by sats from kind-9735 receipts:

- js/zapTotals.js: per-pointer receipt aggregation. The feed's metric getter
  returns the cached total and schedules a batched fetch (#a/#e chunks via
  the L1 SubscriptionManager) for unknown/stale pointers; amounts parse via
  nip57 bolt11 with the zap request's amount tag as fallback; receipts are
  deduped by id so re-fetches never double-count; zero-receipt answers are
  cached (2-min TTL).
- sorters.js: shared count-ranked sorter (metric desc, recency tie-break,
  author spread, trusted-muted sink); Trending delegates unchanged, Most
  Zapped ranks on runtime.getZapTotal.
- Full wiring: feed definition, view + info popover, change-signal re-rank
  (order settles as receipts stream in), sidebar link, viewManager registry,
  feedCoordinator case/loader, FEED_TYPES.MOST_ZAPPED, and a
  FEATURE_MOST_ZAPPED_FEED flag (default on; off hides the tab).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 21:38:53 -04:00

127 lines
4.1 KiB
JavaScript

// js/feedEngine/mostZappedFeed.js
//
// The "Most Zapped" tab (#47): the recently-added (active) source ranked by
// ZAP TOTAL (sats from kind-9735 receipts). Mirrors trendingFeed.js — lives
// outside feedCoordinator.js (size cap) and is wired in via thin delegations.
// The metric getter uses requestVideoZapTotal, which returns the cached total
// AND schedules a batched receipt fetch for unknown pointers; the Most Zapped
// view re-runs the feed on zapTotals' change signal so the order settles as
// totals stream in.
import { FEED_TYPES } from "../constants.js";
import { requestVideoZapTotal } from "../zapTotals.js";
import {
createActiveNostrSource,
createBlacklistFilterStage,
createDedupeByRootStage,
createModerationStage,
createResolvePostedAtStage,
} from "./index.js";
import { createMostZappedSorter } from "./sorters.js";
// Resolve a video's zap total via the app's canonical pointer derivation (the
// same pointer keys the zap flow tags receipts with, so ranking and receipts
// agree).
function makeGetZapTotal(app) {
return (video) => {
if (!video || typeof video !== "object") {
return 0;
}
try {
const info =
typeof app.deriveVideoPointerInfo === "function"
? app.deriveVideoPointerInfo(video)
: null;
if (!info?.pointer) {
return 0;
}
const total = requestVideoZapTotal(info.pointer);
return Number.isFinite(total) ? Number(total) : 0;
} catch (error) {
return 0;
}
};
}
export function buildMostZappedFeedRuntime(app) {
const blacklist =
app.blacklistedEventIds instanceof Set
? new Set(app.blacklistedEventIds)
: new Set();
const moderationThresholds =
typeof app.getActiveModerationThresholds === "function"
? app.getActiveModerationThresholds()
: null;
return {
blacklistedEventIds: blacklist,
isAuthorBlocked: (pubkey) => app.isAuthorBlocked(pubkey),
moderationThresholds: moderationThresholds
? { ...moderationThresholds }
: undefined,
getZapTotal: makeGetZapTotal(app),
};
}
export function registerMostZappedFeed(app) {
if (!app?.feedEngine || typeof app.feedEngine.registerFeed !== "function") {
return null;
}
const existing =
typeof app.feedEngine.getFeedDefinition === "function"
? app.feedEngine.getFeedDefinition(FEED_TYPES.MOST_ZAPPED)
: null;
if (existing) {
return existing;
}
const resolveThreshold = (key) => ({ runtimeValue, defaultValue }) => {
if (
Number.isFinite(runtimeValue) ||
runtimeValue === Number.POSITIVE_INFINITY
) {
return runtimeValue;
}
if (typeof app.getActiveModerationThresholds === "function") {
const active = app.getActiveModerationThresholds();
const candidate =
active && typeof active === "object" ? active[key] : undefined;
if (Number.isFinite(candidate) || candidate === Number.POSITIVE_INFINITY) {
return candidate;
}
}
return defaultValue;
};
try {
return app.feedEngine.registerFeed(FEED_TYPES.MOST_ZAPPED, {
source: createActiveNostrSource({ service: app.nostrService }),
stages: [
createBlacklistFilterStage({
shouldIncludeVideo: (video, options) =>
app.nostrService.shouldIncludeVideo(video, options),
}),
createDedupeByRootStage({
dedupe: (videos) => app.dedupeVideosByRoot(videos),
}),
createModerationStage({
getService: () => app.nostrService.getModerationService(),
autoplayThreshold: resolveThreshold("autoplayBlockThreshold"),
blurThreshold: resolveThreshold("blurThreshold"),
trustedMuteHideThreshold: resolveThreshold("trustedMuteHideThreshold"),
trustedReportHideThreshold: resolveThreshold("trustedSpamHideThreshold"),
}),
createResolvePostedAtStage(),
],
sorter: createMostZappedSorter(),
hooks: {
timestamps: {
getKnownVideoPostedAt: (video) => app.getKnownVideoPostedAt(video),
resolveVideoPostedAt: (video) => app.resolveVideoPostedAt(video),
},
},
});
} catch (error) {
return null;
}
}